C++ Part I · Module 18 · Templates and Generic Programming
Generic Programming as a Design Approach
Nine chapters have each measured what a template facility does and what it costs, and none has asked when reaching for one is the right decision. That question is this chapter's, and it has two halves that pull in opposite directions. The constructive half is a way of thinking that the standard library is built on and that this module has been demonstrating without naming: you do not write code for a type, you write it for a set of operations, and any type supplying those operations is admitted. Chapter 18.2 measured the consequence precisely — a template requiring nothing but a less-than comparison accepted a type with no default constructor, no copy constructor and no assignment operator, because none of those appeared in the body. That is a genuinely different design stance from everything in the preceding modules, where a function named the type it worked on. The critical half is the one the enthusiasm for templates tends to skip. The requirements cannot be stated where a reader would look for them, so they live in a comment that nothing checks. The diagnostic when they are not met reached three hundred and seventy lines in the previous chapter but three. The code grows once per type and so does the build. And a surprising number of templates in real codebases are generic over exactly one type, which is the case where all of that was paid for nothing.
Advanced19 min readC++Part IFundamentalsTemplates
Part I · Module 18 · Chapter 18.10 · Generic Programming as a Design Approach
1. The Stance
Every function before this module named the type it worked on. void drive(ReferenceModel&) works on a ReferenceModel; Chapter 17's void drive(Model&) works on anything deriving from Model. Both name a type.
A template names none:
template <typename T>
const T& smaller(const T& a, const T& b) { return b < a ? b : a; }Generic programming is writing against the operations a type must support, rather than against a type.
The admission criterion is behavioural, not nominal: anything that supports < is accepted, whether or not its author ever heard of smaller.
2. What That Buys, Measured
Chapter 18.2 §3 measured how little a template actually demands:
A. a type with ONLY operator< works:
smaller({7},{3}).v = 3JustLess had no default constructor, no copy constructor, no assignment operator — just a friend operator<. It was admitted, because none of the others appeared in the body.
Three consequences follow, and the third is the one Chapter 17 could not offer.
The requirement is minimal by construction. Chapter 18.2 §3's rule — the requirements are the operations in the body, no more and no less — means a template cannot accidentally demand more than it uses.
The cost is zero. Chapter 18.1 §5 measured the instantiation as five instructions byte-identical to a hand-written version.
And it works on types that can derive from nothing. Chapter 18.1 §9's row: int, double, a vendor's struct. std::sort has to work on a std::vector<int>, and int will never implement an interface — which is why the standard library's algorithms are templates and not a hierarchy.
3. Naming the Operations
The discipline that makes this work is writing the operations down — Chapter 18.2 §5's comment, done properly:
// Requires: T supports `a < b`, usable as a condition.
// Returns: a reference to the smaller argument; a if they compare equal.
template <typename T>
const T& smaller(const T& a, const T& b);The standard library formalises these into named sets — EqualityComparable, LessThanComparable, Copyable, Iterator — and the value of a name is that it is a checkable claim rather than a vague one. "Requires a comparable type" drifts; "requires a < b" can be verified against the body line by line.
The practice that keeps a requirement honest is to read the body and list what it does. If the body gained a swap(a,b) last month and the comment did not, the comment is already wrong — and nothing will tell you, which is §4.
4. The Cost That Is Not Code Size
Chapter 18.1 §6 priced the code: 180 → 413 → 843 bytes for one, three and eight instantiations. That is the cost people quote and it is rarely the one that hurts.
The ones that hurt:
The requirement is unenforceable. Chapter 18.2 §4 established that no C++17 construct states it in the declaration. A comment is the only place it can live, and this curriculum has measured repeatedly what unchecked agreements do — Chapter 15.3 §2 found 12,273 disagreements out of 19,425 between two functions that were supposed to agree with nothing binding them.
The diagnostic is the user's problem, not yours. Chapter 18.7 §7 measured 370 lines, 20 errors, 64 instantiation notes when a type did not satisfy std::sort — first error inside libc++, two lines of the user's own file named anywhere.
Most of the template has never been compiled. Chapter 18.4 §7 measured a member function containing outright nonsense compiling because nobody called it. A class template with twenty members used three ways has checked a fraction of itself.
And build time grows per instantiation, which Chapter 18.6 §6 identified and which compounds with header-only distribution (Chapter 18.7 §4).
5. Stating the Requirement So the Compiler Quotes It
The highest-leverage habit in this module, measured. The unguarded failure:
std::sort on a type with no operator<:
total diagnostic lines: 370 error lines: 20 'in instantiation of' notes: 64Now the same failure with the requirement stated:
template <typename T, typename = void> struct has_less : std::false_type {};
template <typename T> struct has_less<T,
decltype(void(std::declval<const T&>() < std::declval<const T&>()))>
: std::true_type {};
template <typename T> void mySort(T*, T*) {
static_assert(has_less<T>::value, "mySort requires T to support a < b");
// ...
} total diagnostic lines: 7 errors: 1
error: static assertion failed due to requirement 'has_less<NoLess, void>::value':
mySort requires T to support a < b370 lines down to 7, with the message I wrote.
Two things about the detection idiom, because it is worth recognising rather than memorising. It is a partial specialisation — Chapter 18.8 §3's pattern matching — whose second argument is a type computed from an expression; if the expression is ill-formed the specialisation does not match and the primary's false_type stands. And it is exactly the machinery C++20's concepts replace:
template <std::totally_ordered T> void mySort(T*, T*); // C++20So the C++17 practice is: a static_assert at the top of any template that is part of an interface others call. Three lines, and it converts an archaeology exercise into a sentence.
6. Checking What You Claim to Support
Chapter 18.4 §7's gap — most of a class template is uncompiled — has a two-line fix that Chapter 18.6 §8 measured:
template class Stack<int>; // compiles EVERY member
template class Stack<std::unique_ptr<int>>;Explicit instantiation of the class instantiates all its member bodies. One line per type you claim to support, in a test file, turns an untested claim into a compile-time check.
That pairs with §5 to give the whole quality story for a template you own: static_assert says what you require, and explicit instantiation proves you deliver it for the types you advertise.
7. if constexpr Instead of Branching on Types
When behaviour must vary, Chapter 18.8 §9 measured the C++17 answer:
pointer to 7
integral 42
something elseAll three branches compiled with 0 warnings, each valid only for the types reaching it — and with a plain if the untaken branches are compiled and the build fails:
error: cast from pointer to smaller type 'int' loses informationThat is one function with the variation visible in one place, rather than a set of specialisations scattered through a header with Chapter 18.8 §6's overload-resolution trap waiting. For a generic design, if constexpr is usually the right tool for "this type needs different treatment".
8. When Genericity Is Not Earning It
The half the frozen purpose insists on, and it is not a token caveat.
Generic over exactly one type. The commonest case in real codebases: a template instantiated with int and nothing else, ever. It has paid the unenforceable requirement, the bad diagnostics and the header placement, and bought nothing. The test is to grep for the instantiations. One means it should be a function.
Two types that would have been two functions. A template whose body is three lines, used with int and double, replaces six lines with a template plus a comment plus a static_assert. Chapter 18.1 §2's drift argument applies at five copies and not at two.
A requirement list longer than the body. If the operations a type must support take four lines to describe and the body takes two, the abstraction is not paying.
Genericity in an interface others implement against. Chapter 17 is the better mechanism when the set is open and the contract should be stated rather than discovered — Chapter 18.1 §9's table.
And genericity used to avoid a decision. Chapter 10.9 §5's shape, which this curriculum has met for shared_ptr and for inheritance: a template parameter added because it was not clear which type was wanted postpones the decision without resolving it, and every instantiation site then makes it separately.
9. Choosing the Mechanism
Collecting the whole curriculum's answer to one piece of code, many types:
| Condition | Mechanism |
|---|---|
| the type set is open — extended after your code compiles | a hierarchy (Module 17) |
| the type set is closed and known at compile time | a template |
the types cannot derive from anything — int, a vendor struct | a template |
| exactly one type, now and foreseeably | a plain function |
| two or three types, a short body | overloads |
| the behaviour varies on a compile-time property | if constexpr |
| a fixed set of alternatives that must be stored | std::variant (Module 21) |
| one operation, any callable | a callable parameter (Chapter 15.6 §10) |
Chapter 18.1 §9's measured basis for the first two rows: a hierarchy costs an indirection and one function; a template costs code size and no indirection. Chapter 17.1 §5's byte-identical client object file is the capability only a hierarchy has.
10. The Standard Library as the Worked Example
Modules 19 and 20 are built on this chapter's stance, and it is worth naming the pattern before meeting it.
std::sort requires that its iterators can be compared, dereferenced and advanced, and that the elements support < or a supplied comparator. It names no container, no element type and no comparator type. It works on a std::vector<int>, a built-in array, a std::deque<Transaction> — and it rejects a type without < in 370 lines, which is the cost of having asked for nothing by name.
Every requirement is behavioural and every one is documented rather than enforced (before C++20). That is this chapter's stance applied at scale, with its benefits and its diagnostics both.
11. What to Actually Do
Write against operations, not types — and derive the operation list from the body.
State the requirements in a comment, naming operations, and keep it accurate as the body changes.
Put a static_assert in any template others call — measured, 370 diagnostic lines down to 7.
Explicitly instantiate every type you claim to support, in a test — it compiles every member.
Use if constexpr for type-dependent behaviour rather than scattered specialisations.
Grep the instantiations before writing a template — one means write a function.
Use a hierarchy when the type set is open, and a template when it is closed.
And remember C++20's concepts are the real fix for the requirement problem; the static_assert is the C++17 approximation.
12. Mistakes Worth Making Once
A template used with exactly one type. All the author-side costs, no benefit.
A template replacing two six-line functions. The drift argument starts at five copies, not two.
Shipping a template with no static_assert. The user gets 370 lines instead of your sentence.
Shipping a class template with no explicit instantiations in a test. Most of it has never compiled.
A requirement comment longer than the body. The abstraction is not paying.
A requirement comment that drifted from the body. Nothing checks it.
Scattered specialisations where if constexpr would do. Chapter 18.8 §6's trap.
A template parameter added to avoid deciding which type you wanted. Chapter 10.9 §5's shape.
Using a template where the type arrives at run time. Not possible — Chapter 18.1 §9.
Erasing types to get genericity. Chapter 18.1 §3 measured 2.5 where 1.5 was correct.
13. Interview Relevance
"What is generic programming?" Writing against the operations a type must support rather than against a named type. The strong answer gives the admission criterion as behavioural rather than nominal.
"What does a template require of its type?" Exactly the operations in its body — measured, a type with only operator< was accepted despite having no copy constructor or assignment.
"Where are those requirements stated?" Nowhere the compiler checks, before C++20 — which is why a static_assert is the C++17 practice.
"What does that buy?" Measured: the same failure went from 370 diagnostic lines to 7, with your own message.
"How do you know a class template works for a type?" Explicit instantiation — it compiles every member, which a normal call does not.
"When is a template the wrong choice?" One instantiation; two short functions; an open type set (use a hierarchy); a requirement list longer than the body.
"What is the real cost of templates?" Code size and build time are the quoted ones; the unenforceable requirement and the user-facing diagnostics are the expensive ones.
14. What You Should Be Able to Say Now
- Generic programming writes against operations, not types — the admission criterion is behavioural.
- Measured: a template requiring only
<accepted a type with no default constructor, no copy, no assignment. - A template cannot demand more than its body uses — Chapter 18.2 §3's rule.
- Measured: the instantiation was byte-identical to hand-written code.
- It works on types that can derive from nothing — which is why
std::sortis a template. - State the requirements as operations in a comment, derived from the body.
- Measured cost: 180 → 843 bytes for one to eight instantiations — the quoted cost, rarely the painful one.
- The painful costs are author-side: an unenforceable requirement, a mostly-uncompiled template, header-only build time, and a user-facing diagnostic.
- Measured: the unguarded failure was 370 lines, 20 errors, 64 notes, first error inside libc++.
- Measured: a
static_assertreduced it to 7 lines, 1 error, with my own message. - The detection idiom is a partial specialisation on a computed type — Chapter 18.8 §3's pattern matching — and is what concepts replace.
- Measured: explicit instantiation compiles every member, closing Chapter 18.4 §7's gap.
- Measured:
if constexprdiscards untaken branches; a plainiffails to compile them. - Genericity is not earning it with one instantiation, two short functions, an over-long requirement list, an open type set, or a deferred decision.
- The benefits are at the point of use and the costs at the point of definition — which is why "grep the instantiations" is the right test.
- And the alternative to type-checked genericity is worse — Chapter 18.1 §3's
2.5where1.5was correct.
15. Exercises
Exercise 1 — state the stance. Define generic programming in one sentence without using the word template.
Exercise 2 — derive the requirements. For a template computing (a + b) / 2, list every operation T must support, and write the comment.
Exercise 3 — the minimal type. Define a type satisfying your template and nothing else, and say what it proves.
Exercise 4 — guard it. Add a static_assert for one of those operations and report the before and after diagnostic lengths.
Exercise 5 — prove delivery. Write the explicit instantiations for three types you claim to support, and say what they check that calls do not.
Exercise 6 — modernise. Replace a two-way specialisation with if constexpr and say what trap you avoided.
Exercise 7 — audit. Take three templates from a codebase, count the instantiations of each, and say which should be plain functions.
Exercise 8 — choose. For five designs, pick from the §9 table and give the deciding condition.
Exercise 9 — the asymmetry. Explain why the benefits of genericity accrue to callers and the costs to authors, and what that implies for the decision.
16. Summary
- Generic programming writes against operations rather than types, admitting any type that supplies them.
- Measured: a template needing only
<accepted a type with no copy constructor, no assignment and no default constructor. - Measured: zero runtime cost, byte-identical to hand-written code, and it works on types that can derive from nothing.
- The quoted cost is code size — 180 → 843 bytes — and the real costs are author-side: an unenforceable requirement, a mostly-uncompiled definition, header-only build time, and a 370-line user-facing diagnostic.
- Measured: a
static_asserttook that diagnostic to 7 lines with a message I wrote — the highest-leverage habit in this module. - Measured: explicit instantiation compiles every member, turning a claim into a compile error.
if constexprbeats scattered specialisations for type-dependent behaviour.- Genericity is not earning it with one instantiation, two short functions, an open type set, or a deferred decision — so grep the instantiations.
- The benefits are at the point of use, the costs at the point of definition, and that asymmetry is the test.
That completes Module 18, and with it the two mechanisms C++ offers for writing one piece of code that serves many types. Module 17's hierarchies decide at run time and accept types added after compilation; this module's templates decide at compile time, cost nothing per call, and work on types that can inherit from nothing. Between them they account for essentially every abstraction in the standard library, and the frozen description of this module gives the reason it came before the two that follow: every container and algorithm in the next two modules is a template.
Module 19 is Standard Library Containers. std::vector<T> is Chapter 18.4's class template with Chapter 18.5's exact-storage discipline and the Rule of Zero from Chapter 13.11; std::array<T, N> is a non-type parameter doing the work this module's fifth chapter argued for; and the containers' requirements on their element types are Chapter 18.2 §3's operations-not-types, documented and unenforced, which is why putting an unsuitable type in one produces the diagnostics measured here rather than a clear refusal. Knowing that a container is a template — that std::vector<int> and std::vector<Transaction> are separately generated, unrelated types with independently compiled members — is most of what makes the next module's behaviour predictable instead of magical.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
