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.
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.
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).
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'.
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.
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.
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:
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).
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.
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:
[&x] — capture only x, by reference.[x] — capture x by value (a copy).[] — capture nothing.[&] — capture all local names used, by reference.[=] — capture all local names used, by value.[this] — capture the current object by reference (inside a member function, so
class members are referable); [*this] captures a copy of the current object.
[i, this] (as used with
expect() in §4.5).
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 generic — auto 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
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.
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.
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.
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).
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>
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 (...) { /* ... */ }
}
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 |
Which statement best describes a C++ template?
Why does Vector<thread> v2; fail to compile with
template<Element T> class Vector?
What does Buffer<char,1024> glob; buy you over a dynamically allocated
buffer?
What is the element type of Vector vs1 {"Hello", "World"};?
What is the relationship between Less_than{42} and the lambda
[&](int a){ return a<x; }?
Inside a member function, what does [this] capture, and why?
Why can't a function template be a virtual member function?
What does
if constexpr (is_trivially_copyable_v<T>) simple_and_fast(target); else
slow_and_safe(target);
do at compile time?
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
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
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
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
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
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
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
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
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.