Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Templates I: functions that match

Why templates

Templates let a single definition work for many types. The compiler creates a concrete version for each type that appears in a program. This mechanism is compile‑time polymorphism. Runtime polymorphism uses virtual functions, a v‑table, and an indirection. Templates eliminate both indirection and heap allocation. The standard library builds every container and algorithm from templates. std::vector<T> and std::ranges::sort illustrate this fact.

Read templates as term-rewriting rules with unification, in the sense a Prolog programmer knows. A template is a pattern. The compiler unifies the call’s argument types against the pattern’s parameters, binding T to a concrete type, then rewrites the call into a specialized instance. Overload resolution and specialization are rule precedence on top of that unification. This mental model explains why error messages name a failed unification rather than a line in your logic.

Templates let library authors write code that works for any iterator type, raw pointers, std::vector iterators, or user‑defined iterators. The compiler enforces required operations at each instantiation and reports errors attached to the generated code, providing early feedback. Because each specialization is generated at compile time, the optimizer can inline, eliminate dead code, and propagate constants, yielding zero‑overhead performance comparable to hand‑written code. This combination of type‑safe generic programming and compile‑time abstraction makes templates the preferred tool for reusable library components.

Function templates

A function template starts with the keyword template. The following example defines a generic max function.

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

The compiler deduces T from the arguments at the call site. When deduction fails, the caller can supply the type explicitly, for example max<int>(a, b). Deduction works for built‑in types, standard library types, and user‑defined types that provide the required operators.

Deduction can fail or surprise. max(1, 2.0) is ambiguous because T cannot be both int and double. The fix is an explicit max<double>(1, 2.0) or converting the arguments first. The same ambiguity appears whenever two arguments disagree in type, which is why generic helpers often take const T& and let the caller supply matching types.

Template argument deduction follows a set of deduction guides. When a parameter is a reference, the reference qualifiers are preserved. When a parameter is a forwarding reference (T&&), deduction yields an lvalue reference for lvalue arguments and an rvalue reference for rvalue arguments. This mechanism underlies perfect forwarding, a technique used throughout the standard library.

The example program calls max with two int values and prints the result.

#include <print>

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

int main() {
    int a = 3;
    int b = 9;
    std::println("max = {}", max(a, b));
}

Class templates

Class templates use the same syntax as function templates. The example defines a simple wrapper Box that stores a value of type T.

template<typename T>
struct Box {
    T value;
};

std::vector<T> (introduced in chapter 11) is the canonical class template. A class template can contain member functions, static data, and nested type definitions that also depend on the template parameters.

Class template argument deduction (CTAD) lets the compiler infer T from the constructor arguments, so Box(42) deduces Box<int> without writing the angle brackets. A deduction guide can teach the compiler a non-obvious mapping, for example deducing a Box<std::string> from a string literal. CTAD removes the boilerplate that earlier C++ required for every generic constructor call.

The example program creates a Box<int> and a Box<std::string> and prints each value.

#include <print>
#include <string>

template<typename T>
struct Box {
    T value;
};

int main() {
    Box<int> ibox{42};
    Box<std::string> sbox{"hello"};
    std::println("int box: {}", ibox.value);
    std::println("string box: {}", sbox.value);
}

A member variable inside a class template is instantiated once per distinct template argument. For instance, a class template can define int count to provide a separate count for each T. This pattern supports compile‑time registries with zero runtime allocation.

Abbreviated function templates (C++20)

C++20 introduced a shorthand for simple function templates. The declaration auto f(auto x) expands to template<typename T> auto f(T x). The following program demonstrates both forms.

// full form
template<typename T>
auto identity_full(T x) { return x; }

// abbreviated form
auto identity_abbrev(auto x) { return x; }

Both functions return their argument unchanged. The abbreviated form reduces boilerplate and improves readability when the function body does not depend on the template parameter name.

#include <print>

template<typename T>
auto identity_full(T x) { return x; }

auto identity_abbrev(auto x) { return x; }

int main() {
    std::println("full = {}", identity_full(7));
    std::println("abbrev = {}", identity_abbrev(7));
}

Abbreviated templates also integrate with generic lambdas. A lambda such as [](auto x){ return x } is internally a function template, allowing the same zero‑overhead semantics.

An auto parameter in a template is a forwarding reference when it is a template parameter: template<typename T> void f(T&& x) and void f(auto&& x) both bind T to the value category of the argument, so std::forward<T>(x) preserves whether the caller passed an lvalue or an rvalue. This is the mechanism behind std::make_unique and the perfect-forwarding wrapper from chapter 08. A plain T&& that is not a template parameter is an rvalue reference, not a forwarding reference, so the distinction lives in the template parameter list.

Templates also accept a variable number of type parameters with a parameter pack: template<typename... Ts> struct Tuple { }. The ellipsis ... both declares the pack and expands it. Fold expressions collapse a pack with a binary operator, so ((std::cout << args << ' ') ...) prints every argument without writing a loop or a recursive base case. Pack expansion is the compile-time counterpart of a variadic function, and it produces a fully inlined call sequence.

For example, a single function can sum a pack with (0 + ... + args), or print every argument with ((std::cout << args << ' ') ...). The compiler expands the pattern into std::cout << a << ' ' , std::cout << b << ' ' and so on, with no recursion and no runtime dispatch. This is the modern replacement for the recursive variadic helpers that older C++ required.

Dependent names and the typename/template keywords

When a name depends on a template parameter, the compiler cannot know whether it is a type or a value. The following snippet shows two common gotchas.

template<typename T>
void foo(T t) {
    // dependent type requires 'typename'
    typename T::type *ptr = nullptr;
    (void)ptr; // silence unused variable warning
    // dependent member template requires 'template'
    t.template bar<double>();
}

The typename keyword disambiguates a dependent type. The template keyword disambiguates a dependent member template. Without these qualifiers the code fails to compile. Experienced developers encounter these errors frequently when writing generic libraries.

#include <print>

struct HasType {
    using type = int;
    template<typename U>
    void bar() { std::println("bar<{}> called", typeid(U).name()); }
};

template<typename T>
void demo(T t) {
    // Dependent type requires 'typename'
    typename T::type *ptr = nullptr;
    (void)ptr; // silence unused
    // Dependent member template requires 'template'
    t.template bar<double>();
}

int main() {
    HasType obj;
    demo(obj);
    std::println("dependent demo ok");
}

A practical illustration is std::vector<T>::iterator. Inside a template that works with an arbitrary container, writing typename C::iterator it is required. The compiler treats iterator as a static member.

Instantiation

The compiler generates concrete code when a template is used. This process is called implicit instantiation. Each distinct set of template arguments triggers a separate instantiation. Implicit instantiation occurs at the point of first use. Errors inside the template body appear only when a particular specialization is needed.

The late-error property has a downside. Because a member is only compiled when instantiated, a typo in an unused branch of a template can escape every build until a caller instantiates that branch. Concepts (chapter 17) address this by checking constraints before instantiation, so mismatches surface at the call rather than deep inside a library.

Explicit instantiation forces the compiler to emit code for a given set of arguments, even if the program never mentions the specialization. This technique can reduce compile time in large builds because the implementation can be compiled once and reused across translation units.

Templates are not free for the build system. Every instantiation produces object code, so a template used with many distinct types can increase binary size and compile time. Explicit instantiation and the extern template declaration control this growth, which matters in large code bases where template-heavy headers are included widely.

extern template class std::vector<int>; // suppress implicit instantiation

A matching template class std::vector<int> line in another translation unit triggers explicit instantiation. The example program demonstrates explicit instantiation and prints the size of a vector to prove that the code was generated.

#include <vector>
#include <print>

// Explicit instantiation of std::vector<int>
template class std::vector<int>;

int main() {
    std::vector<int> v{1,2,3};
    std::println("size = {}", v.size());
    return 0;
}

Explicit instantiation also interacts with the one‑definition rule. The definition must appear in exactly one translation unit. Otherwise the linker reports multiple definition errors. This rule reinforces the importance of a clear build structure.

The examples instantiate at runtime, but templates also compute at compile time. Chapters 18 and 21 use this to perform type‑level computation and static dispatch. Templates can take non‑type parameters, for example an integer size in std::array<T, N>, so the length is known at compile time. Chapter 19 covers this in detail.

Try this

Write a template clamp that limits a value to a closed interval.

template<typename T>
T clamp(T v, T lo, T hi) { /* return lo if v < lo, hi if v > hi, otherwise v */ }

Call it with a value below lo, a value between lo and hi, and a value above hi.