C++ Function Templates

A function template is a blueprint for a function that works with any data type instead of one fixed type. Rather than writing separate maxInt, maxDouble, and maxChar functions that all do the exact same comparison, you write the logic once and let the compiler generate the type-specific version it needs, automatically, at compile time. Templates are the foundation of generic programming in C++ and power the entire Standard Template Library (STL) — every time you call std::sort or std::max, you are using a function template.

Overview / How It Works

Without templates, writing type-generic code in C++ means either duplicating code for every type, using function overloading (which still requires a separate definition per type), or using void pointers and casts (which throws away type safety). Function templates solve this by letting the type itself become a parameter of the function, alongside its normal value parameters.

A function template is not a function. It is a pattern the compiler uses to stamp out real functions. When you write template <typename T> T myMax(T a, T b), no code is generated yet — the compiler simply records the pattern. Only when your program actually calls myMax(3, 7) does the compiler look at the argument types (here, both int), substitute T with int everywhere in the pattern, and generate a real function called something like myMax<int>(int, int). This process is called template instantiation, and it happens entirely at compile time — there is zero runtime overhead compared to hand-writing the type-specific function yourself.

If you later call myMax(3.5, 2.1) in the same program, the compiler instantiates a second, completely separate function, myMax<double>(double, double). Each distinct set of template arguments used in your program produces its own independent compiled function. This is why templates are sometimes described as a "compile-time code generator" — the compiler is effectively writing overloads for you, but only for the exact types you actually use, keeping the final binary lean and avoiding unused instantiations.

The process the compiler uses to figure out T from your function call arguments (without you having to write myMax<int>(3, 7) explicitly) is called template argument deduction. It looks at the types of the arguments you passed and tries to find a single consistent type for each template parameter. This is powerful, but it is also where most beginner errors with function templates come from, as you will see in the Common Mistakes section.

Syntax

template <typename T>
ReturnType functionName(T param1, T param2, ...) {
    // function body using T as if it were a real type
}
Part Meaning
template Keyword that begins a template declaration.
<typename T> Declares T as a placeholder type parameter. You can use class instead of typename here — they mean exactly the same thing in this position.
T The placeholder name. Used anywhere inside the function as if it were a concrete type (for parameters, local variables, or the return type).
ReturnType Often T itself, but can be any type, including a different template parameter or auto.
functionName<Type>(args) Explicit instantiation syntax, used when the compiler cannot deduce the type on its own, or when you want to force a specific type.

You can declare more than one type parameter by separating them with commas: template <typename T1, typename T2>. Each parameter is deduced independently from the corresponding function arguments.

Examples

Example 1: A generic maximum function

#include <iostream>
using namespace std;

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

int main() {
    cout << "Max of 3, 7: " << myMax(3, 7) << endl;
    cout << "Max of 3.5, 2.1: " << myMax(3.5, 2.1) << endl;
    cout << "Max of 'a', 'z': " << myMax('a', 'z') << endl;
    return 0;
}

Output:

Max of 3, 7: 7
Max of 3.5, 2.1: 3.5
Max of 'a', 'z': z

One template definition serves int, double, and char here. The compiler deduces T separately for each call based on the arguments’ types, and generates three distinct functions behind the scenes. The only requirement is that T supports the > operator, which all three built-in types do.

Example 2: Multiple type parameters and a generic swap

#include <iostream>
#include <string>
using namespace std;

template <typename T1, typename T2>
void printPair(const T1& first, const T2& second) {
    cout << "(" << first << ", " << second << ")" << endl;
}

template <typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    printPair(1, "one");
    printPair(3.14, 'x');

    int x = 10, y = 20;
    mySwap(x, y);
    cout << "x = " << x << ", y = " << y << endl;

    string s1 = "hello", s2 = "world";
    mySwap(s1, s2);
    cout << "s1 = " << s1 << ", s2 = " << s2 << endl;

    return 0;
}

Output:

(1, one)
(3.14, x)
x = 20, y = 10
s1 = world, s2 = hello

printPair uses two independent template parameters, T1 and T2, because the two values passed do not have to share a type. mySwap takes its parameters by reference (T&) so it can modify the caller’s actual variables — this same template works for int and for string without any changes, because T temp = a; and the assignments are valid for any type that supports copying.

Example 3: A template working with a container

#include <iostream>
#include <vector>
using namespace std;

template <typename T>
T findMax(const vector<T>& values) {
    T maxVal = values.at(0);
    for (const T& v : values) {
        if (v > maxVal) {
            maxVal = v;
        }
    }
    return maxVal;
}

int main() {
    vector<int> scores = {42, 88, 15, 99, 61};
    vector<double> prices = {19.99, 5.49, 42.00, 3.25};

    cout << "Highest score: " << findMax(scores) << endl;
    cout << "Highest price: " << findMax(prices) << endl;
    cout << "Explicit call: " << findMax<int>(scores) << endl;

    return 0;
}

Output:

Highest score: 99
Highest price: 42
Explicit call: 99

This is a more realistic use of templates: a single algorithm, findMax, that works on a vector of any comparable type. Notice the vector is taken by const& to avoid an expensive copy of potentially large data, and the loop variable v is also a const T& for the same reason. The last call shows the explicit instantiation syntax, findMax<int>(scores), which is optional here since deduction already works, but is available whenever you want to be explicit or deduction fails.

Under the Hood: Instantiation Step by Step

When the compiler encounters a call like myMax(3.5, 2.1), it performs roughly these steps:

  • 1. Deduction: it inspects the argument types (double, double) and infers T = double.
  • 2. Substitution: it substitutes double for every occurrence of T in the template, producing a candidate function double myMax(double a, double b).
  • 3. Instantiation: if this candidate compiles cleanly, the compiler generates real machine code for it, as if you had written it by hand.
  • 4. Linking against the call site: the call myMax(3.5, 2.1) is compiled to invoke this freshly generated function.

Because instantiation happens per translation unit at compile time, the template’s full definition (not just a declaration) must be visible everywhere it is used — this is why templates are almost always written entirely in header files rather than split into a .h declaration and a .cpp definition.

Common Mistakes

Mistake 1: Ambiguous type deduction from mismatched arguments

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

int main() {
    cout << myMax(3, 3.5) << endl; // error: T deduced as both int and double
    return 0;
}

Here T is used for both parameters, so the compiler tries to deduce a single type from two different argument types (int and double) and fails with a "no matching function" / deduction conflict error. Fix it by forcing the type explicitly:

#include <iostream>
using namespace std;

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

int main() {
    cout << myMax<double>(3, 3.5) << endl;
    return 0;
}

Output:

3.5

Supplying <double> explicitly tells the compiler to convert the int argument to double and instantiate only the double version. Alternatively, you could declare two independent parameters, template <typename T1, typename T2>, if the function genuinely needs to accept mixed types.

Mistake 2: Splitting a template’s declaration and definition across files

// mymath.h
template <typename T>
T square(T x);

// mymath.cpp
template <typename T>
T square(T x) {
    return x * x;
}

// main.cpp
#include "mymath.h"
int main() {
    return square(5); // linker error: undefined reference to square<int>
}

Unlike ordinary functions, a template’s definition must be visible at every call site, because the compiler needs the full body to instantiate it for whatever type is used. Putting only the definition in mymath.cpp means main.cpp never sees it, and the linker cannot find the instantiated symbol. The fix is to put the full template definition in the header file itself, so any file that includes the header can trigger instantiation.

Best Practices

  • Keep the complete template definition in a header file (not split into a .cpp), since every translation unit that calls it needs the full body.
  • Pass large or non-trivial types like std::vector or std::string by const T& to avoid unnecessary copies, and use T& only when the function genuinely needs to modify the caller’s argument.
  • Prefer letting the compiler deduce template arguments from the function call; use explicit func<Type>(...) syntax only when deduction is ambiguous or when you specifically want a different type than would be deduced.
  • Use separate type parameters (T1, T2, …) whenever the arguments are not guaranteed to share a type.
  • Give template parameters descriptive names in complex templates (e.g. Key, Value) instead of always using single letters, once a template has more than one or two parameters.
  • Remember that a template only compiles successfully for a given type if every operation used inside it (like >, *, or <<) is actually supported by that type — templates are checked per instantiation, not in the abstract.
  • Favor a well-tested function template over writing near-duplicate overloads by hand; it keeps behavior consistent and reduces maintenance.

Practice Exercises

  • Exercise 1: Write a function template T myMin(T a, T b) that returns the smaller of two values. Test it with two ints, two doubles, and two strings.
  • Exercise 2: Write a function template void printArray(const vector<T>& arr) that prints every element of a vector of any type, separated by spaces, followed by a newline. Test it with a vector<int> and a vector<string>.
  • Exercise 3: Write a function template bool isEqual(T1 a, T2 b) with two independent type parameters that returns true if a == b. Call it once comparing an int to a double (e.g. 5 and 5.0) and confirm it compiles and returns true.

Summary

  • A function template is a pattern, written once with template <typename T>, that the compiler uses to generate real, type-specific functions at compile time — a process called instantiation.
  • Template argument deduction lets the compiler infer T from the function call’s arguments, but every use of the same parameter name must deduce to the same type.
  • You can declare multiple independent type parameters (T1, T2, …) when arguments are not required to share a type.
  • Template definitions must be fully visible wherever they are called, which is why they live in header files rather than split declaration/definition files.
  • Instantiation only succeeds if every operation used inside the template is valid for the type it is instantiated with.
  • Templates eliminate code duplication with zero runtime overhead, and are the mechanism behind most of the C++ Standard Library.