Calling function templates

In the previous lesson, we created a function template:

template <typename T>
T square(T value)
{
    return value * value;
}

A template alone does nothing. It's a blueprint waiting to be used. This lesson covers how to turn that blueprint into actual functions.

Explicit template arguments

To call a function template, provide the type in angle brackets:

square<int>(5);       // Generate and call square for int
square<double>(3.14); // Generate and call square for double

Complete example:

#include <iostream>

template <typename T>
T square(T value)
{
    return value * value;
}

int main()
{
    std::cout << square<int>(7) << '\n';       // 49
    std::cout << square<double>(2.5) << '\n';  // 6.25

    return 0;
}

Output:

49
6.25

When the compiler sees square<int>(7), it checks if a function square(int) exists. Finding none, it uses the template to create one by substituting int for every T.

The instantiation process

Template instantiation is the compiler's process of generating a concrete function from a template. The result is called a function instance or specialization.

The compiler performs a textual substitution:

// Template
template <typename T>
T square(T value)
{
    return value * value;
}

// Generated for square<int>
int square(int value)
{
    return value * value;
}

// Generated for square<double>
double square(double value)
{
    return value * value;
}

Each unique type produces one instantiation. Calling square<int>(7) and square<int>(12) both use the same generated function.

Type deduction

Writing the type explicitly is often redundant:

square<int>(7);  // We're passing an int, asking for int

The compiler can figure this out automatically through template argument deduction:

square(7);    // Compiler deduces T = int from the argument
square(2.5);  // Compiler deduces T = double from the argument

Both produce the same instantiations as the explicit versions.

Full example with deduction:

#include <iostream>

template <typename T>
T square(T value)
{
    return value * value;
}

int main()
{
    std::cout << square(8) << '\n';     // Deduces int, prints 64
    std::cout << square(1.5) << '\n';   // Deduces double, prints 2.25
    std::cout << square(3.0f) << '\n';  // Deduces float, prints 9

    return 0;
}

Empty angle brackets

There's a third syntax using empty brackets:

square<int>(7);  // Explicit type
square<>(7);     // Deduce type, only consider templates
square(7);       // Deduce type, consider all functions

The difference between square<>() and square() matters when both a template and a regular function could match:

#include <iostream>

template <typename T>
T square(T value)
{
    std::cout << "template\n";
    return value * value;
}

int square(int value)
{
    std::cout << "regular function\n";
    return value * value;
}

int main()
{
    square<int>(5);  // Calls template version
    square<>(5);     // Calls template version (regular functions excluded)
    square(5);       // Calls regular function (preferred over template)

    return 0;
}

When a regular function and a template instantiation match equally well, the regular function wins. This allows specialized implementations to override generic templates.

When deduction fails

Deduction requires the compiler to determine a single consistent type for each template parameter. Conflicting information causes failure:

template <typename T>
T larger(T a, T b)
{
    return (a > b) ? a : b;
}

int main()
{
    larger(10, 20);      // OK: both int, T = int
    larger(1.5, 2.5);    // OK: both double, T = double
    larger(10, 2.5);     // Error: int vs double, T is ambiguous

    return 0;
}

The call larger(10, 2.5) fails because the first argument suggests T = int while the second suggests T = double. The compiler won't guess which you intended.

Solutions:

larger<double>(10, 2.5);  // Explicit: use double, convert 10 to 10.0
larger(10.0, 2.5);        // Make both arguments double

Mixed template and regular parameters

Templates can combine placeholder types with fixed types:

template <typename T>
T scale(T value, double factor)
{
    return static_cast<T>(value * factor);
}

int main()
{
    scale(100, 0.5);      // T = int, result is 50
    scale(3.14, 2.0);     // T = double, result is 6.28
    scale(10, 1.5);       // T = int, result is 15

    return 0;
}

The first parameter determines T. The second parameter is always double.

Templates that fail to compile

A template compiles only if the generated code is valid. Consider:

template <typename T>
T square(T value)
{
    return value * value;
}

This works for numeric types but fails for types that don't support multiplication:

#include <string>

int main()
{
    std::string text{ "hello" };
    square(text);  // Error: can't multiply strings

    return 0;
}

The compiler attempts to generate:

std::string square(std::string value)
{
    return value * value;  // Error: no operator* for string
}

Since std::string doesn't support *, compilation fails. This is expected behavior. Templates work with types that support the operations used in the template body.

Syntactically valid but semantically wrong

Some instantiations compile but produce nonsensical results:

#include <iostream>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne("hello") << '\n';  // Compiles but does pointer arithmetic

    return 0;
}

This compiles because adding an integer to a pointer (which is what a string literal decays to) is syntactically valid. The result is pointer arithmetic, not string concatenation. The output is ello because the pointer advances past the first character.

The compiler can't detect semantic errors. It's your responsibility to use templates with appropriate types.

Summary

Syntax Behavior
square<int>(5) Explicit type specification
square<>(5) Deduce type, only templates considered
square(5) Deduce type, prefer regular functions

Key concepts:

  • Instantiation: Generating a concrete function from a template
  • Type deduction: Compiler infers template arguments from function arguments
  • Deduction failure: Occurs when arguments suggest conflicting types
  • Templates compile only if the generated code is valid for that type
  • Regular functions are preferred over templates when both match equally