Templates & Generics

Scope: A template is a recipe the compiler turns into concrete code for each type you use. Templates let you write one max function that works for int, double, or your own types — this is how the STL containers achieve their type-safety. No C knowledge is assumed: everything builds from what you learned so far.

Function templates

A function template is written once, with a placeholder type. The compiler generates a specific version for every type it is called with.

template <typename T>      // T is the placeholder type
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7) << "\n";        // T = int
    std::cout << maximum(2.5, 1.5) << "\n";  // T = double
    return 0;
}

The compiler instantiates two separate functions here — one per T. That is why templates achieve speed: each uses the exact type's operators, with no runtime overhead.

Type deduction

Normally you do not name T; the compiler deduces it from the arguments. When the arguments disagree, say so explicitly:

maximum(1, 2.5);                 // error: T cannot be both int and double
maximum<double>(1, 2.5);        // OK: T is forced to double

A template only compiles for types that support the operations it uses. maximum requires > and the ternary — every type you pass must provide them.

Class templates

A class template parameterizes a whole class — this is exactly how std::vector<int> and std::vector<double> can share one implementation. You write the template once and name the type at the point of use:

template <typename T>
class Box {
public:
    explicit Box(T value) : value(value) {}
    T get() const { return value; }
private:
    T value;
};

Box<int>    intBox(42);       // T = int
Box<double> dblBox(3.14);     // T = double
std::cout << intBox.get() << " " << dblBox.get();

The angle brackets at the call site are mandatory for class templates — there are no arguments for the compiler to deduce the type from.

Concepts

A template accepts any type that satisfies its operations — that is flexible but loose. A concept (C++20) names the requirements explicitly, so misuse fails with a clear message instead of a wall of template errors:

#include <concepts>

template <std::integral T>      // T must be an integer type
T square(T x) {
    return x * x;
}

square(4);       // OK: int is integral
// square(2.5);  // error: double is not integral — clear message

Use concepts when the requirement matters (numbers, containers, comparable types). For simple internal helpers, a plain typename template is fine.

Template pitfalls

Templates move work to compile time, so their failure modes differ from ordinary functions:

  • Verbose errors — an invalid instantiation produces a long cascade. Read the first "required from here" line; concepts fix most cases.
  • Code bloat — every distinct type instantiation produces machine code. Do not create hundreds of specializations of a heavy template.
  • Header-only reality — templates are usually defined in headers because the compiler must see the body to instantiate. The build systems lesson explains header organization.
  • Explicit instantiation — template Box<int>; compiles the version now, which can shorten build times in big projects.

Practice

  1. Write a minimum function template and test it with int, double, and std::string.
  2. Create a Pair<F, S> class template holding two values of possibly different types.
  3. Add a static_assert inside a template that requires sizeof(T) >= 4 and observe the error for char.
  4. Replace the assert with a C++20 std::integral concept and compare the error quality.