Chapter 7 introduced templates as "for all T" — but most templates only make sense for types
that meet specific requirements: a Vector needs copyable elements,
sum() needs a sequence and a number. Chapter 8 is where those requirements become
first-class language citizens. Concepts (C++20) are compile-time predicates
that name what a template argument must provide — turning unreadable instantiation-time errors
into clear messages at the point of use, and enabling concept-based overloading, constrained
auto, and disciplined generic programming. The second half of the chapter covers
variadic templates (arbitrary numbers of arguments of arbitrary types),
fold expressions, argument forwarding, and the template
compilation model that explains why "template definitions live in header files."
Why concepts matter now: this is the payoff chapter for everything since
§7.2.1. Templates are C++'s main support for generic programming — parametric
polymorphism at compile time — and concepts are what make generic code checkable, readable,
and overloadable. Interviewers love probing the sharp corners here: where concepts
check arguments (point of use) vs. where
the template body gets checked (instantiation), what
requires requires means, and how concept-based overload resolution picks
advance(). This lesson goes deep on all of them.
Templates offer three distinct powers:
Buffer is the example).
The first and most common use of templates is to support generic programming: programming focused on the design, implementation, and use of general algorithms — algorithms that accept a wide variety of types as long as they meet the algorithm's requirements on its arguments. Together with concepts, the template is C++'s main support for generic programming; templates provide (compile-time) parametric polymorphism. Alex Stepanov's epigraph for the chapter sets the priority: "Programming: you have to start with interesting algorithms."
Consider the sum() from §7.3.1:
template<typename Seq, typename Value>
Value sum(Seq s, Value v) {
for (const auto& x : s)
v += x;
return v;
}
For this to compile and behave correctly, sum() requires that its first argument
be some kind of sequence of elements and its second be some kind of number.
More specifically:
Seq must support begin() and end() so the range-for
works (§1.7, §14.1) — such types are called ranges: vector,
list, map.
Value must be an arithmetic type supporting += so
elements can be added — int, double, Matrix (for a
reasonable definition of Matrix).
We call such requirements concepts. The algorithm is generic in two dimensions: the type of the data structure holding the elements ("the sequence") and the type of the elements themselves.
Most template arguments must meet specific requirements for the template to compile — most
templates should be constrained templates (§7.2.1). The introducer
typename is the least constraining; it requires only that the argument
be a type. Usually we can do better:
template<Sequence Seq, Number Num>
Num sum(Seq s, Num v) {
for (const auto& x : s)
v += x;
return v;
}
Once Sequence and Number are defined, the compiler can reject bad
calls by looking at sum()'s interface only, rather than its
implementation — a huge improvement in error reporting. But this specification is still
incomplete: it doesn't say we can add elements of a Sequence to a
Number. We can:
template<Sequence Seq, Number Num>
requires Arithmetic<range_value_t<Seq>, Num>
Num sum(Seq s, Num n);
range_value_t<Seq> is the type of a sequence's elements (§16.4.4);
Arithmetic<X,Y> says arithmetic works between X and
Y. This saves us from accidentally summing a vector<string> or
a vector<int*> while still accepting vector<int> and
vector<complex<double>>. The general lesson: when an algorithm
requires arguments of differing types, there is usually a relationship between those
types that it's good to make explicit.
Two design notes from Stroustrup. Don't constrain too tightly: we needed only
+=, but using the general Arithmetic leaves room to later express
sum() in terms of + and = instead.
Partial specifications are useful: unless a specification is complete, some errors
still surface at instantiation time, but even partial concepts express intent and support
incremental development.
requires Arithmetic<...> is a requires-clause. The
template<Sequence Seq> notation is shorthand for the explicit
requires Sequence<Seq>; both notations combine:
template<typename Seq, typename Num>
requires Sequence<Seq> && Number<Num> && Arithmetic<range_value_t<Seq>, Num>
Num sum(Seq s, Num n);
// equivalently, folding the constraint into the parameter list:
template<Sequence Seq, Arithmetic<range_value_t<Seq>> Num>
Num sum(Seq s, Num n);
In code bases that can't use concepts yet, the fallback is naming conventions and comments —
// requires Arithmetic<range_value_t<Sequence>,Number>. Whatever the
notation, design templates with semantically meaningful constraints on their arguments
(§8.2.4).
Once templates have proper interfaces, we can overload on their properties, much as
we overload functions. A simplified standard advance() that moves an iterator
n elements forward has two implementations — one for iterators with only
++, one for iterators with +=:
template<forward_iterator Iter> // a forward iterator has ++, but not + or +=
void advance(Iter p, int n) {
while (n--) ++p;
}
template<random_access_iterator Iter> // a random-access iterator has +=
void advance(Iter p, int n) {
p += n;
}
The compiler selects the template with the strongest requirements met by the
arguments. A list offers only forward iterators; a vector offers
random-access iterators:
void user(vector<int>::iterator vip, list<string>::iterator lsp) {
advance(vip, 10); // uses the fast advance() (p += n)
advance(lsp, 10); // uses the slow advance() (while (n--) ++p)
}
Like all overloading this is a compile-time mechanism — no run-time cost — and when the compiler finds no best choice it reports an ambiguity error. The rules are far simpler than general overloading (§1.3). For a single argument among several alternatives:
For an alternative to win overall it must be a match for all of its arguments, at least an equally good match for all arguments as other alternatives, and a better match for at least one argument.
The question of whether template arguments offer what a template requires ultimately boils down to whether some expressions are valid. A requires-expression checks exactly that:
template<forward_iterator Iter>
requires requires(Iter p, int i) { p[i]; p + i; } // Iter has subscripting and integer addition
void advance(Iter p, int n) {
p += n;
}
No, that requires requires is not a typo. The first requires starts
the requires-clause; the second starts the requires-expression. A
requires-expression is a predicate: true if the statements in its body are valid
code, false otherwise.
Stroustrup calls requires-expressions "the assembly code of generic programming": extremely
flexible, imposing no discipline, at the bottom of most interesting generic code — and like
assembly, they shouldn't appear in ordinary code. They belong inside the implementation of
abstractions. If you see requires requires in your code, it's probably too low
level and will eventually become a problem. The example above is deliberately hackish — it
"forgot" to specify += and the required return types, so some uses pass concept
checking yet still fail to compile. Prefer properly named concepts with well-specified
semantics, and use requires-expressions primarily in the definitions of those
concepts.
Useful concepts (like forward_iterator) come from libraries, including the
standard library (§14.5) — using one is easier than writing your own. But simple concepts are
not hard to define. A concept is a compile-time predicate specifying how one
or more types can be used:
template<typename T>
concept Equality_comparable = requires (T a, T b) {
{ a == b } -> Boolean; // compare Ts with ==
{ a != b } -> Boolean; // compare Ts with !=
};
The value of a concept is always bool. The
{ ... } -> Concept form says "this expression is valid and its result
satisfies Concept" — here Boolean, a user-defined concept meaning "usable as a
condition" (there is no standard-library boolean concept, so Stroustrup defined one). Testing:
static_assert(Equality_comparable<int>); // succeeds
struct S { int a; };
static_assert(Equality_comparable<S>); // fails: structs don't automatically get == and !=
Handling non-homogeneous comparisons is almost as easy — note the
default template argument T2 = T, used when the second argument isn't
given:
template<typename T, typename T2 = T>
concept Equality_comparable = requires (T a, T2 b) {
{ a == b } -> Boolean;
{ a != b } -> Boolean;
{ b == a } -> Boolean; // both directions, so int == double and double == int both work
{ b != a } -> Boolean;
};
static_assert(Equality_comparable<int, double>); // succeeds
static_assert(Equality_comparable<int>); // succeeds (T2 defaults to int)
static_assert(Equality_comparable<int, string>); // fails
A Number concept checks arithmetic operations and a zero, making no assumptions
about result types — adequate for simple uses:
template<typename T, typename U = T>
concept Number = requires(T x, U y) {
x+y; x-y; x*y; x/y; // the four arithmetic operations
x+=y; x-=y; x*=y; x/=y;
x = x; // copyable
x = 0; // has a zero
};
Number<X> checks X alone;
Number<X,Y> checks the two types work together. From that, the symmetric
Arithmetic concept used in §8.2.1:
template<typename T, typename U = T>
concept Arithmetic = Number<T, U> && Number<U, T>; // arithmetic works in both directions
A Sequence concept is more complex — it must provide a value type, an iterator
type, begin()/end()
returning that iterator, and the iterator must be at least an
input_iterator with matching value type:
template<typename S>
concept Sequence = requires (S a) {
typename range_value_t<S>; // S must have a value type
typename iterator_t<S>; // S must have an iterator type
{ a.begin() } -> same_as<iterator_t<S>>; // begin() returns the iterator type
{ a.end() } -> same_as<iterator_t<S>>;
requires input_iterator<iterator_t<S>>; // the iterator is usable
requires same_as<range_value_t<S>, iter_value_t<S>>;
};
The typename X; form inside a requires-expression checks that a
type exists; requires C; (nested) checks a further constraint. The
hardest concepts to define are fundamental language concepts, so prefer an established
library's set (§14.5) — the whole Sequence above collapses to
concept Sequence = input_range<S>;. For simple cases, an alias hides
complexity:
template<class S> using Value_type = typename S::value_type; // simple, if S has that member
This is the subtle part, and interviewers love it. The concepts specified for a template are used to check arguments at the point of use — they are not used to check the use of the parameters in the template's definition:
template<equality_comparable T>
bool cmp(T a, T b) {
return a < b; // the concept guarantees == and !=, but NOT <
}
bool b0 = cmp(cout, cerr); // error: ostream doesn't support == (caught at point of use)
bool b1 = cmp(2, 3); // OK: int supports == and returns true
bool b2 = cmp(2+3i, 3+4i); // error: complex<double> supports == but not <
// (caught at instantiation time, when the body is checked)
The concept check accepts the ints and complex<double>s (both
have ==) — but int has < so
cmp(2,3) compiles, while the < in the body fails when the body is
instantiated for complex<double>. Delaying the final check of the template
definition until instantiation has two benefits: we can use
incomplete concepts during development (gradually improving checking), and we can
insert debug/tracing/ telemetry code into a template without changing its interface (interface
changes cause massive recompilation). The price: some errors are caught very late in the
compilation process, with spectacularly bad messages (§8.5).
auto denotes the least constrained concept — it requires only that the
value be a value of some type. Taking an auto parameter makes a function
into a function template. Concepts strengthen all such uses:
auto twice(Arithmetic auto x) { return x + x; } // just for numbers
auto thrice(auto x) { return x + x + x; } // for anything with a +
auto x1 = twice(7); // OK: x1 == 14
string s = "Hello ";
auto x2 = twice(s); // error: a string is not Arithmetic
auto x3 = thrice(s); // OK: x3 == "Hello Hello Hello "
Concepts can also constrain variable initialization and return types:
auto ch1 = open_channel("foo"); // works with whatever open_channel() returns
Arithmetic auto ch2 = open_channel("foo"); // error: a channel is not Arithmetic
Channel auto ch3 = open_channel("foo"); // OK: assuming Channel is an appropriate concept
Number auto some_function(int x) {
// ...
return fct(x); // an error unless fct(x) returns a Number
}
Constraining a return type catches a type error as close to its origin as possible — better for readability and debugging than the equivalent (verbose, possibly copy-heavy) local variable.
A type specifies the set of operations applicable to an object (implicitly and explicitly), relies on function declarations and language rules, and specifies how an object is laid out in memory. A single-argument concept specifies the set of operations too, but relies on use patterns reflecting function declarations and language rules, says nothing about layout, and enables a set of types. Concepts therefore give more flexibility than types, and can express relationships among several arguments. Stroustrup's ideal: eventually most functions will be template functions with arguments constrained by concepts. The notational support isn't perfect — a concept must be used as an adjective, not a noun:
void sort(Sortable auto&); // OK: 'auto' required
void sort(Sortable&); // error: 'auto' required after concept name
The form of generic programming C++ directly supports: abstract from concrete, efficient algorithms to obtain generic algorithms that combine with different data representations to produce a wide variety of useful software. The abstractions representing fundamental operations and data structures are called concepts [Stepanov, 2009].
Good concepts are fundamental and discovered more than designed —
integer, floating-point, sequence, and mathematical
notions like ring and vector space represent fundamental concepts of
a field of application (hence the name). For basic use, the standard concept
regular (§14.5) describes types that behave much like an int or a
vector: an object of regular type can be default constructed; copied (yielding
two independent objects that compare equal); compared with == and
!=; and doesn't suffer technical problems from overly clever programming tricks.
string is regular — and also totally_ordered (§14.5): comparable
with <, <=, >, >=,
<=> with the appropriate semantics.
A concept is not just syntactic — it is fundamentally about
semantics. Don't define + to divide; that wouldn't match any reasonable
number. We don't yet have language support for expressing semantics, so we rely on expert
knowledge and common sense — and we should not define semantically meaningless
concepts such as Addable and Subtractable. Rely on domain knowledge
to define concepts matching fundamental concepts in an application domain.
Good abstractions are carefully grown from concrete examples — not invented by preparing for every conceivable need (that direction leads to inelegance and code bloat). Start with one — preferably more — concrete examples from real use and eliminate inessential details:
double sum(const vector<int>& v) {
double res = 0;
for (auto x : v)
res += x;
return res;
}
What makes this less general than it needs to be? Why just
ints? Why just vectors? Why accumulate in a double? Why
start at 0? Why add? Answering the first four by making the concrete types template arguments
gives the standard library's accumulate (§17.3):
template<forward_iterator Iter, Arithmetic<iter_value_t<Iter>> Val>
Val accumulate(Iter first, Iter last, Val res) {
for (auto p = first; p != last; ++p)
res += *p;
return res;
}
The data structure is abstracted into a pair of iterators representing a sequence;
the accumulator's type is a parameter (and must be arithmetic, working with the iterator's
value type); the initial value is now an input whose type is the accumulator's type. Code
generated for calls with a variety of data structures is identical to hand-coded examples. The
process of generalizing from concrete code while preserving performance is called
lifting. The best way to develop a template: first write a concrete version,
then debug/test/measure it, finally replace concrete types with template arguments. The
repeated begin()/end() is tedious, so a range-based overload
simplifies common use:
template<forward_range R, Arithmetic<value_type_t<R>> Val>
Val accumulate(const R& r, Val res = 0) {
for (auto x : r)
res += x;
return res;
}
void use(const vector<int>& vec, const list<double>& lst) {
auto sum = accumulate(begin(vec), end(vec), 0.0); // accumulate in a double
auto sum2 = accumulate(begin(lst), end(lst), sum);
}
Both forms are useful: pair-of-iterators for generality, range for simplicity. For full
generality, the += operation itself can be abstracted (§17.3).
A template can accept an arbitrary number of arguments of arbitrary types — a
variadic template. A simple function printing values of any type with a
<< operator:
void user() {
print("first: ", 1, 2.2, "hello\n"s); // first: 1 2.2 hello
print("\nsecond: ", 0.2, 'c', "yuck!"s, 0, 1, 2, '\n');
}
The traditional implementation separates the first argument (the head) from the rest (the tail) and recursively calls itself on the tail:
template<typename T>
concept Printable = requires(T t) { std::cout << t; } // just one operation!
void print() { } // what we do for no arguments: nothing
template<Printable T, Printable... Tail>
void print(T head, Tail... tail) {
cout << head << ' '; // first, what we do for the head
print(tail...); // then, what we do for the tail
}
Printable... indicates that Tail is a sequence of types;
Tail... indicates that tail is a sequence of values of those types.
A parameter declared with ... is a parameter pack. Each call
splits into head + tail; eventually the tail is empty and the no-argument
print() handles it. To forbid the zero-argument case (or avoid generating that
final call), use a compile-time if:
template<Printable T, Printable... Tail>
void print(T head, Tail... tail) {
cout << head << ' ';
if constexpr (sizeof...(tail) > 0) // compile-time: no print() call generated for the last head
print(tail...);
}
The strength of variadic templates — accepting any arguments — comes with weaknesses: the recursive implementations can be tricky to get right; the interface's type checking is a possibly elaborate template program, ad hoc rather than defined in the standard; and the recursion can be surprisingly expensive in compile time and compiler memory. They're widely used in the standard library, and occasionally wildly overused.
For simple variadic templates, C++ offers a limited form of iteration over a parameter pack — fold expressions:
template<Number... T>
int sum(T... v) {
return (v + ... + 0); // add all elements of v starting with 0
}
int x = sum(1, 2, 3, 4, 5); // x becomes 15
int y = sum('a', 2.4, x); // y becomes 114 ('a' is 97; 2.4 is truncated)
(v + ... + 0) adds all elements starting from the rightmost:
(v[0]+(v[1]+(v[2]+(v[3]+(v[4]+0))))) — a right fold. The operator order
swaps for a left fold:
template<Number... T>
int sum2(T... v) {
return (0 + ... + v); // (((((0+v[0])+v[1])+v[2])+v[3])+v[4])
}
Folds are a powerful abstraction, closely related to
accumulate() — and they need not be numeric. The famous print-with-fold:
template<Printable... T>
void print(T&&... args) {
(std::cout << ... << args) << '\n'; // chain the << operations left-to-right
}
print("Hello!"s, ' ', "World ", 2017);
// expands to: (((((std::cout << "Hello!"s) << ' ') << "World ") << 2017) << '\n');
Why 2017 in the example? Because folds were added to C++ in C++17 (§19.2.3).
Passing arguments unchanged through an interface is an important use of variadic
templates. A network InputChannel
whose transport mechanism is a template parameter — different transports need different
constructor arguments:
template<concepts::InputTransport Transport>
class InputChannel {
public:
// ...
InputChannel(TransportArgs&&... transportArgs)
: _transport(std::forward<TransportArgs>(transportArgs)...) {}
// ...
Transport _transport;
};
std::forward (§16.6) moves the arguments unchanged from
InputChannel's constructor to Transport's constructor — the writer
of InputChannel constructs a Transport without knowing what
arguments it requires, needing only the common user interface. Forwarding is common in
foundational libraries where generality and low run-time overhead are necessary and interfaces
are very general.
At the point of use, template arguments are checked against their concepts — errors found here are reported immediately. What can't be checked there (arguments of unconstrained parameters, uses in the template body) is postponed until code is generated for the template with a particular set of arguments: at template instantiation time. The unfortunate side effects: type errors detected uncomfortably late, and spectacularly bad error messages, because the compiler lacks the type information that hints at the programmer's intent and often detects the problem only after combining information from several places in the program.
Instantiation-time checking provides a compile-time variant of what is often called duck typing — "if it walks like a duck and it quacks like a duck, it's a duck." More technically: we operate on values, and the presence and meaning of an operation depend solely on its operand values, rather than on objects' types determining the operations. What is done at compile time using templates mostly does not involve objects, only values.
The practical consequence:
to use an unconstrained template, its definition (not just its declaration) must be in
scope at its point of use.
With header files and #include, that means template definitions live in header
files, not .cpp files — the standard header <vector> holds the
definition of vector. Modules (§3.2.2) change this: with modules, source can be
organized the same way for ordinary functions and template functions, because a module is
semi-compiled into a representation (an easily traversed graph of scope and type information,
with a symbol table) that makes importing fast.
Here is a summary of the guidance from this chapter. All 18 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.
| # | Guideline | § |
|---|---|---|
| 1 | Templates provide a general mechanism for compile-time programming. | 8.1 |
| 2 | When designing a template, carefully consider the concepts (requirements) assumed for its template arguments. | 8.3.2 |
| 3 | When designing a template, use a concrete version for initial implementation, debugging, and measurement. | 8.3.2 |
| 4 | Use concepts as a design tool. | 8.2.1 |
| 5 | Specify concepts for all template arguments. | 8.2 |
| 6 | Whenever possible use named concepts (e.g., standard-library concepts). | 8.2.4, 14.5 |
| 7 | Use a lambda if you need a simple function object in one place only. | 7.3.2 |
| 8 | Use templates to express containers and ranges. | 8.3.2 |
| 9 | Avoid "concepts" without meaningful semantics. | 8.2 |
| 10 | Require a complete set of operations for a concept. | 8.2 |
| 11 | Use named concepts. | 8.2.3 |
| 12 | Avoid requires requires. |
8.2.3 |
| 13 | auto is the least constrained concept. |
8.2.5 |
| 14 | Use variadic templates when you need a function that takes a variable number of arguments of a variety of types. | 8.4 |
| 15 | Templates offer compile-time "duck typing." | 8.5 |
| 16 |
When using header files, #include template definitions (not just
declarations) in every translation unit that uses them.
|
8.5 |
| 17 | To use a template, make sure its definition (not just its declaration) is in scope. | 8.5 |
| 18 | Unconstrained templates offer compile-time "duck typing." | 8.5 |
With template<Sequence Seq, Number Num> Num sum(Seq s, Num v); — when
does the compiler reject a call with a non-sequence or non-number argument?
template<equality_comparable T> bool cmp(T a, T b) { return a < b; }
— which call fails, and where?
What does the double requires in
requires requires(Iter p, int i) { p[i]; p+i; } mean?
Both advance() overloads are viable for a
vector<int>::iterator. Which is chosen, and why?
What does { a == b } -> Boolean; inside a requires-expression require?
In
template<typename T, typename T2 = T> concept Equality_comparable = ...
— what does Equality_comparable<int> check?
Why is Arithmetic defined as
Number<T, U> && Number<U, T> rather than just
Number<T, U>?
Which statement about Arithmetic auto x = open_channel("foo"); is true?
Why does void sort(Sortable&); fail to compile while
void sort(Sortable auto&); works?
How does
template<Number... T> int sum(T... v) { return (v + ... + 0); }
evaluate sum(1, 2, 3)?
Why do template definitions (e.g., vector's) live in header files rather than
.cpp files?
concept ⇒ compile-time predicate on types → what a template argument must provide
checking ⇒ at point of use, interface only → clear early errors
3 template powers ⇒ types/values/templates as args ; context weaving ; compile-time computation
generic programming ⇒ algorithms over types meeting requirements → parametric polymorphism
template<Sequence Seq> ⇒ shorthand for
requires Sequence<Seq>
requires-clause ⇒ explicit constraints (eg
Arithmetic<range_value_t<Seq>,Num>)
typename ⇒ least constraining ; only ''is a type''
don't over-constrain ⇒ general concepts beat narrow requirements (eg +=)
partial specs ⇒ express intent ; some errors still at instantiation
advance() ⇒ forward_iterator (++ only) v/s random_access_iterator (+=)
selection ⇒ strongest requirements met by args wins → compile-time, no cost
rules ⇒ non-match excluded | single match chosen | stricter chosen | equal → ambiguity
requires requires(...) ⇒ clause + expression ; predicate ''is this
code valid?''
use ⇒ in concept definitions ; assembly of generic programming
bare requires requires in code ⇒ too low level ; prefer named concepts
template<typename T> concept X = requires(T a) { {a==b} -> Boolean; }
{expr} -> C ⇒ expr valid AND result satisfies C ; concept value
always bool
default template arg ⇒ typename T2 = T
Arithmetic ⇒ Number<T,U> && Number<U,T>
→ symmetric
Sequence ⇒ value type + iterator type + begin/end + input_iterator ; or
input_range<S>
concepts ⇒ check arguments at point of use
template body ⇒ checked at instantiation time
cmp(2+3i,3+4i) ⇒ passes == concept, fails < in body → late
error
benefits ⇒ incomplete concepts OK ; debug code without interface changes
auto ⇒ least constrained concept ''some type''
Arithmetic auto x = ... ⇒ constrains initialization
→ counters auto overuse
constrained return ⇒ Number auto f() → error near origin
concept = adjective ⇒ Sortable auto& ;
Sortable& error
type ⇒ operations + layout + one type
concept ⇒ operations + semantics, no layout → set of types
semantics matter × Addable/Subtractable
⇒ meaningless
regular ⇒ default-constructible + copyable + == ; behaves like int/vector
lifting ⇒ concrete → generic while preserving performance
recipe ⇒ concrete version → debug/test/measure → parameterize types
accumulate ⇒ pair-of-iterators (general) v/s range (simple)
5 questions ⇒ why ints? vector? double? start 0? add? → template args
parameter pack ⇒ Tail... ; head + tail recursion
termination ⇒ empty print() |
if constexpr(sizeof...(tail) > 0)
fold ⇒ right (v + ... + 0) →
v[0]+(v[1]+(...+0))
left ⇒ (0 + ... + v) → ((0+v[0])+v[1])+... ;
folds = C++17
std::forward ⇒ pass args unchanged through interface
cost ⇒ compile time + memory ; tricky recursion
definition must be in scope at use ⇒ templates in headers, not .cpp
duck typing ⇒ operations depend on values, not declared types
late checking ⇒ atrocious error messages
modules ⇒ fix it → semi-compiled, fast to import
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 8: "Concepts and Generic Programming." Addison-Wesley.
Reference:
Chapter 1–8 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
constraints and concepts,
requires-expression,
fold expressions,
parameter packs, and
std::forward.
Questions? Ask your agent — your teacher — about anything unclear: how definition checking
delays errors to instantiation time, when to write your own concept instead of using the
standard library's, or how
std::forward preserves argument categories. Follow-ups are expected, not
optional.