Updated Jul 14, 2026

Templates & Generic Programming

Go back to Phase 8: Copy, Move & the Rule of Five and Phase 9: Operator Overloading for a second and picture what you've built so far: a Matrix class, say, that overloads +, has proper copy and move constructors, and cleans up after itself with RAII. It's solid. But it only works for Matrix<double>. What if someone needs a matrix of int, or float, or a custom fraction type? Do you copy-paste the whole class and change one word?

That's the problem templates exist to solve, and it's the last big piece of the puzzle before you're fluent in modern C++. Once you have templates, the STL (next two phases) stops being a black box of container types you memorize and becomes something you understand from the inside, because the STL is templates, top to bottom.

The mental model: templates are a blueprint, not a function

Here's the idea that makes everything else click: a template is not code. It's a recipe for generating code. When you write a function template, nothing compiles yet - there's no machine code sitting in your binary for it. The compiler waits until it sees you actually use the template with a specific type, and only then does it generate a real, concrete function for that type. This is called instantiation.

Compare that to what you might expect from other languages. In Python, a function just works with whatever type you throw at it, checked at runtime. In Java, generics are largely a compile-time-only illusion (type erasure) - the bytecode doesn't know or care what T was. C++ templates are neither of those. They generate a real, fully-typed, separately-optimized function or class for every distinct type you instantiate with. Call max(3, 5) and max(3.0, 5.0) in the same program, and the compiler produces two completely separate functions under the hood, each one just as fast as if you'd hand-written it for that exact type. This is why templates are sometimes called "compile-time polymorphism" or "zero-cost generics" - you get the flexibility of generic code with none of the runtime overhead of, say, virtual dispatch (which you'll meet properly in Phase 14: Inheritance & Polymorphism).

The tradeoff is real too: more instantiations means more generated code (longer compile times, bigger binaries in some cases), and template error messages have a well-earned reputation for being long and intimidating - though modern compilers (and C++20 concepts, which constrain templates to give clearer errors) have gotten much better about this.

Function templates

Let's build the thing that motivates all of this: a max function that works for any comparable type.

#include <iostream>

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

int main() {
    std::cout << my_max(3, 7) << "\n";        // T deduced as int
    std::cout << my_max(2.5, 1.1) << "\n";     // T deduced as double
    std::cout << my_max('a', 'z') << "\n";     // T deduced as char
}
7
2.5
z

Read template <typename T> as "for some type T, which I'll figure out or you'll tell me." T is a placeholder - a template parameter - that stands in for a real type. You could write template <class T> instead of typename T; they mean exactly the same thing here, it's purely historical (typename came later, and most style guides now prefer it for clarity). When you call my_max(3, 7), the compiler looks at the argument types, deduces T = int, and generates a version of my_max where every T is replaced with int. Call it again with doubles, and it generates a second, entirely separate version with T = double. Neither version pays any price for the other existing.

When deduction fails or you want to be explicit, you can spell out T yourself:

my_max<double>(3, 7.5);   // force T = double, converting 3 to 3.0

Class templates

The same idea applies to whole classes - this is how you'd finally write that Matrix<T> or a generic Box<T> that holds one value of any type.

template <typename T>
class Box {
public:
    explicit Box(T value) : value_(value) {}

    T get() const { return value_; }
    void set(T value) { value_ = value; }

private:
    T value_;
};

int main() {
    Box<int> b1(42);
    Box<std::string> b2("hello");

    std::cout << b1.get() << " " << b2.get() << "\n";
}
42 hello

Box<int> and Box<std::string> are, to the compiler, two completely unrelated classes - as unrelated as if you'd written BoxInt and BoxString by hand. The template just saved you from writing them by hand. Notice you write out member functions the same way you always have; the only new syntax is the template <typename T> header above the class, and then T is just an ordinary type name everywhere inside it.

You've actually already used a class template without necessarily naming it that: std::vector<int> is vector instantiated with T = int. std::vector itself, uninstantiated, isn't a type at all - it's a template. std::vector<int> and std::vector<double> are two different, unrelated types that happen to share source code.

Multiple template parameters

Templates aren't limited to one placeholder type. A Pair that holds two possibly-different types needs two:

template <typename T1, typename T2>
class Pair {
public:
    Pair(T1 first, T2 second) : first_(first), second_(second) {}

    T1 first() const { return first_; }
    T2 second() const { return second_; }

private:
    T1 first_;
    T2 second_;
};

int main() {
    Pair<std::string, int> age("Alice", 30);
    std::cout << age.first() << " is " << age.second() << "\n";
}

This is, essentially, std::pair - the standard library ships its own version of exactly this, because the pattern is so common.

Non-type template parameters

Template parameters don't have to be types - they can be compile-time values too, most commonly used for fixed-size arrays:

template <typename T, int N>
class FixedArray {
public:
    T& operator[](int i) { return data_[i]; }
    int size() const { return N; }

private:
    T data_[N];
};

int main() {
    FixedArray<double, 5> scores;   // N = 5 baked in at compile time
    scores[0] = 9.5;
    std::cout << scores.size() << "\n";   // 5
}

Here N is a value, not a type, fixed at compile time for each instantiation - FixedArray<double, 5> and FixedArray<double, 10> are different types with different-sized internal arrays, and no heap allocation is needed because the compiler knows the exact size at compile time. std::array<T, N>, which you'll meet in the next phase, is built exactly this way.

Template specialization: an escape hatch for special cases

Sometimes the generic version of a template is wrong, or just inefficient, for one particular type. Specialization lets you say "for this specific type, use this different implementation instead."

template <typename T>
void print_type(T value) {
    std::cout << "generic value: " << value << "\n";
}

// full specialization for bool
template <>
void print_type<bool>(bool value) {
    std::cout << "boolean: " << (value ? "true" : "false") << "\n";
}

int main() {
    print_type(42);     // generic value: 42
    print_type(true);   // boolean: true
}

template <> with no parameters, followed by <bool> after the function name, says "this isn't a new template - it's a hand-written override for exactly T = bool." The compiler prefers the specialization whenever the type matches, and falls back to the generic version otherwise. You won't reach for this often as a beginner, but it's worth recognizing when you see it in library code, because it explains why, say, std::vector<bool> behaves so strangely compared to every other std::vector<T> - it's a specialization with a totally different internal representation (packed bits instead of real bools).

Templates vs. overloading vs. void*

If you came from C, you might reach for void* to write "generic" code - a function that takes any pointer, casts it internally, and hopes for the best. Templates replace that whole pattern and do it safely: no casting, no losing type information, and the compiler catches type errors at the call site instead of you finding out at 2am that you passed the wrong struct. If you came from a language with function overloading, templates are what you reach for when you'd otherwise have to write the same body for every overload - overloading is for genuinely different logic per type; templates are for identical logic, different type.

What error messages look like (and how to read them)

Template errors can be long because the compiler is often reporting the error at the point of instantiation, several layers deep. The trick is to read from the bottom or top consistently (compilers differ) and look for the first concrete type mismatch - the rest is usually the compiler explaining how it got there.

template <typename T>
T add(T a, T b) { return a + b; }

struct Point { int x, y; };

int main() {
    Point p1{1, 2}, p2{3, 4};
    add(p1, p2);   // Point has no operator+
}
error: invalid operands to binary expression ('Point' and 'Point')
    return a + b;
             ^ ~
note: in instantiation of function template specialization 'add<Point>' requested here
    add(p1, p2);
    ^

Read it the same way you learned to read borrow-checker errors if you've seen Rust, or ordinary compile errors elsewhere: the real problem (Point has no +) is stated plainly, and the "in instantiation of" note is just the compiler showing its work - which specific instantiation triggered the failure. Once you know to look for that pattern, template errors stop being scary walls of text.

Recap

  1. A template is not code - it's a blueprint. The compiler generates a real, concrete function or class only when you instantiate it with an actual type, and each instantiation is fully independent, fully optimized code.
  2. template <typename T> (or class T, identical meaning) declares a placeholder type; the compiler usually deduces it from your arguments, or you can specify it explicitly with func<Type>(...).
  3. Class templates work the same way - std::vector<int> is nothing more than vector instantiated with T = int, and it's an unrelated type to std::vector<double>.
  4. Non-type template parameters (like int N) let a compile-time value, not just a type, shape the generated code - the basis for std::array<T, N>.
  5. Specialization (template <>) lets you override the generic implementation for one specific type when the general version is wrong or slow for it.
  6. This is compile-time, zero-runtime-cost generic code - the mechanism the entire STL is built on, which is exactly where you're headed next.

Quick check

Test yourself on the idea that makes everything else in this phase click - that a template generates real, separate code per type, at compile time:

[
  {
    "q": "When you call `my_max(3, 7)` and `my_max(2.5, 1.1)` in the same program, what does the compiler actually produce?",
    "choices": [
      "One generic function that branches on the argument type at runtime",
      "Two completely separate, fully-typed functions - one for `int`, one for `double` - generated at compile time",
      "A single function using `void*` internally, cast back to the right type on each call",
      "Nothing extra - templates are erased before compilation, like Java generics"
    ],
    "answer": 1,
    "explain": "Each distinct type you instantiate a template with gets its own real, separately-compiled function or class - this is why templates carry zero runtime overhead, unlike a branch-on-type function or Java's type erasure."
  },
  {
    "q": "`std::vector<int>` and `std::vector<double>` - what's the relationship between them, according to the compiler?",
    "choices": [
      "They're the same type, just displayed differently",
      "`std::vector<double>` inherits from `std::vector<int>`",
      "They are two completely unrelated types that happen to share the same source code",
      "They share one underlying implementation chosen at runtime"
    ],
    "answer": 2,
    "explain": "A class template isn't a type by itself - `std::vector` only becomes a real type once instantiated with a `T`, and each instantiation is as unrelated to the others as if you'd hand-written separate classes."
  },
  {
    "q": "You write a full specialization `template <> void print_type<bool>(bool value) { ... }` alongside the generic `print_type<T>`. What happens when you call `print_type(true)`?",
    "choices": [
      "A compile error, since you can't have two versions of the same function",
      "The generic version always wins, since it was defined first",
      "The compiler picks the `bool` specialization instead of instantiating the generic version",
      "Both versions run, generic first, then the specialization"
    ],
    "answer": 2,
    "explain": "Specialization is an escape hatch: the compiler prefers a matching specialization over generating a new instantiation from the generic template, which is exactly how `std::vector<bool>`'s odd packed-bit behavior comes about."
  }
]

← Phase 9: Operator Overloading · Phase 11: The STL - Containers →

Check your understanding 3 questions

1. When you call `my_max(3, 7)` and `my_max(2.5, 1.1)` in the same program, what does the compiler actually produce?

2. `std::vector<int>` and `std::vector<double>` - what's the relationship between them, according to the compiler?

3. You write a full specialization `template <> void print_type<bool>(bool value) { ... }` alongside the generic `print_type<T>`. What happens when you call `print_type(true)`?