Lesson 7: Templates

Lesson 0007 — A Tour of C++, Chapter 7 (§7.1–§7.5)

Chapter 6 finished the Vector-of-doubles story — but someone who wants a Vector is unlikely to always want a Vector<double>. This chapter generalizes everything you've built: a template is a class or function parameterized by types or values, and it's the mechanism behind the standard library's vector<int>, vector<string>, map, and algorithms like sort(). You'll see the parameterized Vector<T>, concepts that constrain template arguments, value template arguments, class template argument deduction (CTAD), and the three ways to write a parameterized operation — function templates, function objects, and lambda expressions — plus the supporting mechanisms: variable templates, alias templates, and compile-time if.

Why templates matter now: the standard library you'll meet in later chapters is almost entirely templates — containers, algorithms, smart pointers. Templates are a compile-time mechanism: the code generated for Vector<double> is identical to the hand-written version from Chapter 5, and there is no run-time overhead. Concepts and if constexpr (both C++20) are the modern way to keep that power checked and readable.

7.1 Introduction — Templates

A vector is a general concept, independent of the notion of a floating-point number — so the element type ought to be represented independently. A template is a class or a function that we parameterize with a set of types or values, generating specific types and functions by specifying arguments (such as double as the element type). This chapter focuses on the language mechanisms; Chapter 8 follows with programming techniques, and the library chapters (10–18) offer many examples.

7.2 Parameterized Types

We generalize our vector-of-doubles (§5.2.2) by replacing the specific type double with a type parameter:

template<typename T> class Vector {
private:
    T* elem;              // elem points to an array of sz elements of type T
    int sz;
public:
    explicit Vector(int s);       // constructor: establish invariant, acquire resources
    ~Vector() { delete[] elem; }  // destructor: release resources
    // ... copy and move operations ...
    T& operator[](int i);                 // for non-const Vectors
    const T& operator[](int i) const;     // for const Vectors (§5.2.1)
    int size() const { return sz; }
};

The template<typename T> prefix makes T a type parameter — C++'s version of the mathematical "for all T" (or more precisely, "for all types T"). If you want "for all T, such that P(T)," you use concepts (§7.2.1, §8.2). Using class instead of typename is equivalent; older code often writes template<class T>.

Member functions are defined similarly:

template<typename T> Vector<T>::Vector(int s) {
    if (s < 0) throw length_error{"Vector constructor: negative size"};
    elem = new T[s];
    sz = s;
}
template<typename T> const T& Vector<T>::operator[](int i) const {
    if (i < 0 || size() <= i) throw out_of_range{"Vector::operator[]"};
    return elem[i];
}

Now we can define Vectors of any element type — note the >> in Vector<list<int>> terminates the nested template arguments; it is not a misplaced input operator:

Vector<char> vc(200);           // vector of 200 characters
Vector<string> vs(17);         // vector of 17 strings
Vector<list<int>> vli(45);     // vector of 45 lists of integers

To support range-for, we define free-standing begin() and end() (the "one-past-the-last" convention from §6.5.2):

template<typename T> T* begin(Vector<T>& x) { return &x[0]; }           // first element
template<typename T> T* end(Vector<T>& x)   { return &x[0] + x.size(); } // one-past-the-last

Templates are a compile-time mechanism — their use incurs no run-time overhead compared to hand-crafted code. The code generated for Vector<double> is identical to the Chapter 5 version; the code for the standard-library vector<double> is likely even better, because more effort went into it. A template plus a set of template arguments is an instantiation (or specialization); late in compilation, at instantiation time, code is generated for each instantiation used in the program (§8.5).

7.2.1 Constrained Template Arguments

A template usually only makes sense for arguments meeting certain criteria. A Vector offers copy, so its elements must be copyable. We state that requirement directly:

template<Element T> class Vector {   // T must be an Element
    // ...
};

template<Element T> is C++'s "for all T such that Element(T)": Element is a predicate checking that T has all the properties Vector requires. Such a predicate is a concept (§8.2). A template argument for which a concept is specified is constrained; the template is a constrained template. Using a type that doesn't meet the requirements is a compile-time error:

Vector<int> v1;      // OK: we can copy an int
Vector<thread> v2;   // error: we can't copy a standard thread (§18.2)

Concepts let the compiler type-check at the point of use, giving far better error messages much earlier than unconstrained templates. C++ didn't officially support concepts before C++20, so older code leaves requirements to documentation — and its type checking happens unpleasantly late, at instantiation time, with often atrocious error messages. Concept checking itself is purely compile-time; the generated code is as good as unconstrained templates'.

7.2.2 Value Template Arguments

Templates can also take value arguments — integers, pointers, etc.:

template<typename T, int N> struct Buffer {
    constexpr int size() { return N; }
    T elem[N];
    // ...
};

Value arguments let us create arbitrarily sized buffers with no use of the free store:

Buffer<char,1024> glob;   // global buffer of characters (statically allocated)
void fct() {
    Buffer<int,10> buf;    // local buffer of integers (on the stack)
    // ...
}

One limitation: for obscure technical reasons a string literal cannot (yet) be a template value argument — though an array holding the characters works. In C++, there is usually a workaround; we don't need direct support for every use case.

7.2.3 Template Argument Deduction

When defining a type as a template instantiation we must specify its arguments — which can be tedious. Fortunately, in many contexts the constructor can deduce them from an initializer:

pair<int,double> p = {1, 5.2};   // explicit
pair p = {1, 5.2};                // p is a pair<int,double> (deduced)

Vector v1 {1, 2, 3};              // deduce v1's element type from the initializer: int
Vector v2 = v1;                   // deduce v2's element type from v1's: int
auto p = new Vector{1, 2, 3};     // p is a Vector<int>*
Vector<int> v3(1);                 // here we must be explicit (no element type is mentioned)

Deduction simplifies notation and eliminates mistyped redundant arguments — but like all powerful mechanisms, it can surprise:

Vector<string> vs  {"Hello", "World"};   // OK: Vector<string>
Vector vs1 {"Hello", "World"};           // OK: deduces to Vector<const char*> (Surprise?)
Vector vs2 {"Hello"s, "World"s};         // OK: deduces to Vector<string>
Vector vs3 {"Hello"s, "World"};          // error: the initializer list is not homogeneous
Vector<string> vs4 {"Hello"s, "World"};  // OK: the element type is explicit

The type of a C-style string literal is const char* — if that's not what you intended for vs1, be explicit or use the s suffix (§10.2). If initializer elements have differing types, no unique element type can be deduced, so you get an ambiguity error.

Sometimes a constructor is itself ambiguous. Vector has both an initializer-list constructor and an iterator-pair constructor:

Vector v1 {1, 2, 3, 4, 5};                 // element type is int
Vector v2(v1.begin(), v1.begin()+2);       // a pair of iterators — or a pair of values?
Vector v3(9, 17);                          // error: ambiguous

A deduction guide resolves this by saying "a pair of values of the same type should be considered iterators":

template<typename Iter>
Vector(Iter, Iter) -> Vector<typename Iter::value_type>;

Now Vector v2(v1.begin(), v1.begin()+2) is a pair of iterators with element type int. The {} syntax always prefers the initializer-list constructor if present, so Vector v3 {v1.begin(), v1.begin()+2} is a Vector of iterators; () is conventional when you don't want an initializer_list. Deduction guides have subtle effects, so it's best to design class templates so they aren't needed. Acronym lovers call this "class template argument deduction" — CTAD.

7.3 Parameterized Operations

Templates parameterize much more than containers — they are extensively used for both types and algorithms in the standard library (§12.8, §13.5). There are three ways to express an operation parameterized by types or values:

7.3.1 Function Templates

A function that sums the element values of any sequence a range-for can traverse:

template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v) {
    for (auto x : s)
        v += x;
    return v;
}

The Value template argument and the v argument let the caller choose the accumulator's type and initial value — and the template arguments are deduced from the function arguments:

void user(Vector<int>& vi, list<double>& ld, vector<complex<double>>& vc) {
    int x  = sum(vi, 0);                          // the sum of a vector of ints (add ints)
    double d = sum(vi, 0.0);                      // the sum of a vector of ints (add doubles)
    double dd = sum(ld, 0.0);                     // the sum of a list of doubles
    auto z = sum(vc, complex{0.0, 0.0});          // the sum of a vector of complex<double>s
}

Adding ints into a double gracefully handles a sum larger than the largest int. This sum() is a simplified accumulate() (§17.3). One restriction: a function template can be a member function but not a virtual member — the compiler can't know all instantiations in a program, so it couldn't generate a vtbl (§5.4).

7.3.2 Function Objects

A function object (or functor) is an object that can be called like a function:

template<typename T> class Less_than {
    const T val;                          // value to compare against
public:
    Less_than(const T& v) : val{v} { }
    bool operator()(const T& x) const { return x < val; }   // call operator
};

The function called operator() implements the application operator — "function call." We define named variables of type Less_than for an argument type:

Less_than lti {42};          // lti(i) will compare i to 42 using < (i<42)
Less_than lts {"Backus"s};   // lts(s) will compare s to "Backus" using < (s<"Backus")
Less_than<string> lts2 {"Naur"};  // "Naur" is a C-style string, so we need <string>

Function objects are widely used as arguments to algorithms — e.g., a simplified count_if (§13.5) that counts values for which a predicate returns true:

template<typename C, typename P>
int count(const C& c, P pred) {   // C is a container, P a predicate on its elements
    int cnt = 0;
    for (const auto& x : c)
        if (pred(x)) ++cnt;
    return cnt;
}

void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s) {
    cout << "number of values less than " << x << ": "
         << count(vec, Less_than{x}) << '\n';   // compares to x
    cout << "number of values less than " << s << ": "
         << count(lst, Less_than{s}) << '\n';   // compares to s
}

The beauty of function objects: they carry the value to compare against with them. No separate function per value (and per type), no global variables to hold values — and for a simple object like Less_than, inlining is easy, so a call is far more efficient than an indirect function call. Function objects used to specify the meaning of key operations of a general algorithm are sometimes called policy objects.

7.3.3 Lambda Expressions

Defining Less_than separately from its use is inconvenient. A lambda expression implicitly generates a function object:

void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s) {
    cout << "number of values less than " << x << ": "
         << count(vec, [&](int a){ return a < x; }) << '\n';
    cout << "number of values less than " << s << ": "
         << count(lst, [&](const string& a){ return a < s; }) << '\n';
}

The notation [&](int a){ return a<x; } generates a function object similar to Less_than<int>{x}. The [&] is a capture list: all local names used in the body (like x) are accessed through references. The capture options:

7.3.3.1 Lambdas as Function Arguments

Lambdas are convenient and terse, but can be obscure — for nontrivial actions, prefer naming the operation to state its purpose and make it reusable. They also let us separate traversal from per-element action: a simplified for_each (§13.5) applies an operation to each object pointed to by a container of pointers:

template<typename C, typename Oper>
void for_each(C& c, Oper op) {   // C is a container of pointers
    for (auto& x : c)
        op(x);                   // pass op() a reference to each pointed-to element
}

Now the draw_all()/rotate_all() functions from §5.5 vanish into two lambdas — the pointer elements passed by reference so for_each() never deals with lifetime issues:

void user() {
    vector<unique_ptr<Shape>> v;
    while (cin) v.push_back(read_shape(cin));
    for_each(v, [](unique_ptr<Shape>& ps){ ps->draw(); });      // draw_all()
    for_each(v, [](unique_ptr<Shape>& ps){ ps->rotate(45); });  // rotate_all(45)
}

A lambda can be genericauto parameters make it a template, accepting any type (and constrainable with a concept):

for_each(v, [](auto& s){ s->rotate(r); s->draw(); });   // works for any drawable/rotatable

7.3.3.2 Lambdas for Initialization

A lambda can turn any statement into an expression — mostly used to compute a value as an argument, but the ability is general. Consider a messy switch-based initialization (§6-style):

void user(Init_mode m, int n, vector<int>& arg, Iterator p, Iterator q) {
    vector<int> v;
    switch (m) {
        case zero: v = vector<int>(n);   // n elements initialized to 0
            break;
        case cpy:  v = arg;
            break;
    }
    // ...
    if (m == seq) v.assign(p, q);        // copy from sequence [p:q)
}

Such code is messy and bug-prone: the variable could be used before it gets its intended value; initialization gets mixed with other code; it's easy to forget a case; and this isn't initialization at all — it's assignment (§1.9.2). A lambda-as-initializer fixes it:

void user(Init_mode m, int n, vector<int>& arg, Iterator p, Iterator q) {
    vector<int> v = [&] {
        switch (m) {
            case zero: return vector<int>(n);   // n elements initialized to 0
            case seq:  return vector<int>{p, q}; // copy from sequence [p:q)
            case cpy:  return arg;
        }
    }();
    // ...
}

A forgotten case is now easier to spot, and in many cases the compiler will warn.

7.3.3.3 Finally

Destructors provide a general, implicit cleanup mechanism (RAII, §6.3) — but what about cleanup not associated with a single object, or with an object that has no destructor (e.g., a type shared with a C program)? We can define a finally() function that takes an action to execute on scope exit:

void old_style(int n) {
    void* p = malloc(n * sizeof(int));   // C-style
    auto act = finally([&]{ free(p); }); // call the lambda upon scope exit
    // ...
}   // p is implicitly freed upon scope exit

This is ad hoc, but far better than trying to call free(p) correctly and consistently on all exits. The implementation is trivial — a Final_action whose destructor runs the stored action, and a [[nodiscard]] marker so users can't forget to keep the generated object in scope:

template <class F> [[nodiscard]] auto finally(F f) { return Final_action{f}; }

template <class F> struct Final_action {
    explicit Final_action(F f) : act(f) {}
    ~Final_action() { act(); }
    F act;
};

There's a finally() in the Core Guidelines Support Library (GSL), and a proposal for a more elaborate scope_exit mechanism for the standard library.

7.4 Template Mechanisms

Good templates need supporting facilities: variable templates (§7.4.1), alias templates (§7.4.2), compile-time selection with if constexpr (§7.4.3), and requires-expressions (§8.2.3). constexpr functions (§1.6) and static_asserts (§4.5.2) also take part. These are primarily tools for building general, foundational abstractions.

7.4.1 Variable Templates

When we use a type, we often want constants and values of that type — the same holds for class templates:

template <class T> constexpr T viscosity = 0.4;
template <class T> constexpr space_vector<T> external_acceleration = { T{}, T{-9.8}, T{} };

auto vis2 = 2 * viscosity<double>;
auto acc  = external_acceleration<float>;

Curiously, most variable templates seem to be constants — but then, so are many variables. We can use arbitrary expressions as initializers, which after some significant mutations becomes the heart of concept definitions (§8.2):

template<typename T, typename T2>
constexpr bool Assignable = is_assignable<T&, T2>::value;   // a type trait (§16.4.1)

template<typename T> void testing() {
    static_assert(Assignable<T&, double>, "can't assign a double to a T");
    static_assert(Assignable<T&, string>, "can't assign a string to a T");
}

The standard library uses variable templates for mathematical constants such as pi and log2e (§17.9).

7.4.2 Aliases

Aliases give a synonym for a type or template. The header <cstddef> defines size_t — the actual type is implementation-dependent (unsigned int here, unsigned long there), and the alias makes code portable. Parameterized types often provide aliases for types related to their arguments:

template<typename T> class Vector {
public:
    using value_type = T;   // the type of the elements
    // ...
};

Every standard-library container provides value_type, so code following that convention works for all of them — and aliases can also bind some arguments of an existing template:

template<typename C> using Value_type = C::value_type;   // the type of C's elements

template<typename Key, typename Value> class Map { /* ... */ };
template<typename Value> using String_map = Map<string, Value>;
String_map<int> m;   // m is a Map<string,int>

7.4.3 Compile-Time if

Consider an operation implementable two ways — slow_and_safe(T) or simple_and_fast(T). A class hierarchy can provide the general operation in a base class and override with the fast one; a compile-time if is the template answer:

template<typename T> void update(T& target) {
    // ...
    if constexpr (is_trivially_copyable_v<T>) simple_and_fast(target);  // for "plain old data"
    else                                     slow_and_safe(target);     // for more complex types
    // ...
}

is_trivially_copyable_v<T> is a type predicate (§16.4.1). Only the selected branch is checked by the compiler — optimal performance and locality of optimization.

Importantly, if constexpr is not a text-manipulation mechanism and cannot break the usual rules of grammar, type, and scope. A naive attempt to conditionally wrap a call in a try-block fails:

template<typename T> void bad(T arg) {
    if constexpr(!is_trivially_copyable_v<T>) try {   // Oops, the if extends beyond this line
        g(arg);
    if constexpr(!is_trivially_copyable_v<T>) } catch(...) { /* ... */ }   // syntax error
}

Such text manipulation would compromise readability and break tools relying on modern program representation. Cleaner solutions that respect scope rules are available:

template<typename T> void good(T arg) {
    if constexpr (is_trivially_copyable_v<T>) g(arg);
    else try { g(arg); } catch (...) { /* ... */ }
}

7.5 Advice

Here is a summary of the guidance from this chapter. All 11 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.

# Guideline §
1 Use templates to express algorithms that apply to many argument types. 7.1
2 Use templates to express containers. 7.2
3 Use templates to raise the level of abstraction of code. 7.2
4 Templates are type safe, but for unconstrained templates checking happens too late. 7.2
5 Let constructors or function templates deduce class template argument types. 7.2.3
6 Use function objects as arguments to algorithms. 7.3.2
7 Use a lambda if you need a simple function object in one place only. 7.3.2
8 A virtual function member cannot be a template member function. 7.3.1
9 Use finally() to provide RAII for types without destructors that require "cleanup operations." 7.3.3.3
10 Use template aliases to simplify notation and hide implementation details. 7.4.2
11 Use if constexpr to provide alternative implementations without run-time overhead. 7.4.3

Retrieval Quiz

What a template is

Which statement best describes a C++ template?

Constrained template arguments

Why does Vector<thread> v2; fail to compile with template<Element T> class Vector?

Value template arguments

What does Buffer<char,1024> glob; buy you over a dynamically allocated buffer?

CTAD surprises

What is the element type of Vector vs1 {"Hello", "World"};?

Function objects vs. lambdas

What is the relationship between Less_than{42} and the lambda [&](int a){ return a<x; }?

Lambda capture lists

Inside a member function, what does [this] capture, and why?

Templates and virtual functions

Why can't a function template be a virtual member function?

if constexpr semantics

What does if constexpr (is_trivially_copyable_v<T>) simple_and_fast(target); else slow_and_safe(target); do at compile time?


Notes

Templates :-

template class/function parameterized by types or values

"for all T" math notation generate types by specifying args

compile-time no run-time overhead ; code = hand-crafted quality

instantiation template + args code generated late in compilation

Parameterized types :-

template<typename T> class Vector T = element type

member fns defined with template<typename T> prefix

range-for needs free-standing begin()/end()

Vector<list<int>> >> = nested args, not input op

Constrained arguments :-

concept predicate on T template<Element T>

violation compile-time error at point of use (eg Vector<thread>)

C++20 older code unconstrained late, atrocious errors

concepts purely compile-time ; code as good as unconstrained

Value arguments :-

template<typename T, int N> size as compile-time constant

Buffer<char,1024> static/stack buffer no free store

string literal × value arg (for now) ; array workaround exists

Deduction (CTAD) :-

CTAD constructor deduces args (eg pair p = {1,5.2})

surprise Vector vs1 {"Hello","World"} const char*

mixed types ambiguity error ; be explicit or use s suffix

deduction guide resolves ambiguity Vector(Iter,Iter) -> Vector<Iter::value_type>

{} prefers initializer_list ; () when not wanted

Parameterized operations :-

3 ways function template | function object | lambda

function template args deduced from call (eg sum(vi,0.0))

virtual × template member vtbl needs all instantiations

Function objects :-

functor object callable via operator()

carries data no globals, no per-value functions (eg Less_than{42})

policy object specifies key op of an algorithm (eg count's predicate)

inlining cheaper than indirect call

Lambdas :-

lambda shorthand for function object ; [&](int a){ return a<x; }

captures [&] all by ref | [=] all by value | [] none | [x] one by value | [&x] one by ref

member fn [this] object by ref ; [*this] copy

generic lambda auto param template ; constrain with concept

lambda for init statement expression no messy switches

finally() cleanup for destructor-less types Final_action dtor runs it

Template mechanisms :-

variable template constexpr T viscosity = 0.4; constants per type

alias using value_type = T; Value_type<C>, String_map<Value>

if constexpr only selected branch checked ; not text manipulation


Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed., Chapter 7: "Templates." Addison-Wesley.
Reference: Chapter 1–7 Quick Reference & Glossary — keep it beside you while you study.
Recommended supplement: cppreference on templates, class template argument deduction, lambda expressions, if constexpr, and concepts.

Questions? Ask your agent — your teacher — about anything unclear: how CTAD chooses between constructors, when a lambda is better than a named function object, or how concepts give better errors than unconstrained templates. Follow-ups are expected, not optional.

Chapters

  1. Lesson 1: The Basics (Ch. 1)
  2. Lesson 2: User-Defined Types (Ch. 2)
  3. Lesson 3: Separate Compilation (Ch. 3)
  4. Lesson 4: Error Handling (Ch. 4)
  5. Lesson 5: Classes (Ch. 5)
  6. Lesson 6: Essential Operations (Ch. 6)
  7. Lesson 7: Templates (Ch. 7 — this lesson)
  8. Lesson 8: Concepts and Generic Programming (Ch. 8)
  9. Lesson 9: Library Overview (Ch. 9)
  10. Lesson 10: Strings and Regular Expressions (Ch. 10)
  11. Lesson 11: Input and Output (Ch. 11)
  12. Lesson 12: Containers (Ch. 12)
  13. Lesson 13: Algorithms (Ch. 13)
  14. Lesson 14: Ranges (Ch. 14)
  15. Lesson 15: Pointers and Containers (Ch. 15)
  16. Lesson 16: Utilities (Ch. 16)
  17. Lesson 17: Numerics (Ch. 17)
  18. Lesson 18: Concurrency (Ch. 18)
  19. Lesson 19: History and Compatibility (Ch. 19)
← Course Dashboard ← Ch. 6: Essential Operations Ch. 8: Concepts → Quick Reference →