Programming glossary 81 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
Lesson: Templates in C++ — Generic Code, Constraints & Instantiation
Templates let us write code that is parameterised over types (and values). The template itself is a blueprint; the compiler generates concrete functions/classes only when you use the template with specific arguments (this step is called instantiation). Type checking therefore happens at the points of use, not when the template is first seen.
Succinctly: A template is source code for generating other source code at compile time. The instantiated code is what actually compiles and runs.
Programming Insight (AI) — “Show me the instantiations”
- Paste a TU (translation unit); ask AI to list which template specialisations are instantiated and where, including deduced arguments.
- Ask for a map of errors: which call site triggers which long template diagnostic.
1) Class Templates — your example, modernised
Original idea:
template <typename T>
class C1 {
private:
T* a;
public:
C1(T b) { a = new T; *a = b; } // raw new, no delete → leak & exception-safety issues
};
// int main() {
// C1<int> b(1);
// bool ans = b > 5; // ❌ no operator> for C1<int> vs int
// }
Issues: manual new/delete (Rule of Zero violated) and an attempt to use > without defining it. Here’s a safe, idiomatic version:
#include <memory>
#include <concepts>
template <typename T>
requires std::movable<T> // C++20 concept: constrain what T must support
class Box {
public:
// Constructors
Box() = default;
explicit Box(T value) : ptr_(std::make_unique<T>(std::move(value))) {}
// Observers
const T& get() const { return *ptr_; }
bool has_value() const noexcept { return static_cast<bool>(ptr_); }
// Mutators
void set(T value) { ptr_ = std::make_unique<T>(std::move(value)); }
// Comparisons (only if T is comparable)
friend bool operator==(const Box& a, const Box& b)
requires requires { {*a.ptr_} == {*b.ptr_}; }
{
if (!a.ptr_ || !b.ptr_) return a.ptr_ == b.ptr_;
return *a.ptr_ == *b.ptr_;
}
friend auto operator<=>(const Box& a, const Box& b)
requires requires { {*a.ptr_} <=> {*b.ptr_}; }
= default; // defaulted spaceship when T supports it
private:
std::unique_ptr<T> ptr_{}; // RAII: no raw new/delete
};
This keeps ownership safe, constrains usage with concepts, and only makes comparisons available when T supports them.
Your previously failing line b > 5 still won’t compile (by design), unless you define how a Box<int> compares to an int.
Programming Insight (AI) — Add the right constraints
- Describe intended operations on
T(copy? move? <? ==?). Ask AI to proposerequires-clauses or ready-made concepts. - Get a fallback design when the constraint cannot be satisfied (e.g., customise via policy template parameter).
2) Function Templates — deduction, return type, and mixed arguments
Generic add with safe return type using std::common_type_t and constraints:
#include <type_traits>
#include <concepts>
template <class A, class B>
requires requires(A a, B b) { a + b; } // expression constraint: 'a+b' must compile
auto add(A a, B b) -> std::common_type_t<A,B> {
return a + b; // returned as common type (e.g., int + double → double)
}
// use:
// auto x = add(2, 3.5); // double
This resolves the “int and double are logically fine in one place but not another” concern by defining a single, explicit rule
for the result type. If you need stricter semantics (e.g., disallow narrowing), add stronger concepts or static_asserts.
3) Concepts vs SFINAE — readable constraints win (C++20+)
- Old school: SFINAE (Substitution Failure Is Not An Error) with
std::enable_ifmade error messages cryptic. - Modern: Prefer
requiresand standard concepts (e.g.,std::regular,std::totally_ordered,std::ranges::range).
#include <concepts>
template <std::totally_ordered T>
const T& minv(const T& a, const T& b) { return (b < a) ? b : a; }
Programming Insight (AI) — Decode the template error
- Paste the long diagnostic; ask AI to translate it and point to the offending requirement/call site with a concrete fix.
4) Non-Type Template Parameters (NTTP) — values at compile time
Sizes, policies, and even pointers/references can parameterise templates:
template <typename T, std::size_t N>
struct Fixed {
T data[N]{};
constexpr std::size_t size() const noexcept { return N; }
};
Fixed<int, 16> buf; // N chosen at compile time: no heap, contiguous & cache-friendly
C++20 allows auto NTTPs for literal types — helpful for policies and small compile-time constants.
5) Specialisation — customise behaviour for some arguments
Full specialisation replaces a template for a specific argument set; partial specialisation matches a family.
// Primary
template <typename T> struct Wrapper { using type = T; };
// Partial: pointer types
template <typename T> struct Wrapper<T*> { using type = T; }; // unwrap one level
// Full: exactly 'Wrapper<void>'
template <> struct Wrapper<void> { using type = void; };
Use specialisation to preserve intent across tricky cases (e.g., pointers, string literals) — not as a first resort. Prefer concepts/overloads where possible.
6) Perfect Forwarding (briefly)
Forwarding references keep value category and cv-qualifiers for factory/adapter patterns.
#include <utility>
template <class T, class... Args>
T make_with(Args&&... args) {
return T(std::forward<Args>(args)...);
}
Use sparingly and test carefully; forwarding can amplify overload resolution surprises.
7) Where to put template definitions — “it’s in the header”
- Because instantiation happens at use sites, the compiler must see the definition. Put function/class template definitions in headers, not only in
.cppfiles. - Alternatively, use explicit instantiation in a
.cppto reduce code bloat for known types:// In header: template <typename T> void foo(T); // In .cpp: template void foo<int>(int); // explicit instantiation definition
Programming Insight (AI) — Linker gotchas
- Ask AI to flag templates implemented in
.cppthat aren’t explicitly instantiated → potential “undefined reference” errors.
8) Debugging Templates — make failures readable
- Constrain templates so invalid calls fail early with clear messages (concepts).
- Use
static_assertwith helpful text:static_assert(std::is_integral_v<T>, "Box<T> requires integral T for this overload"); - Break complex templates into helpers; test helpers with concrete types.
9) Performance & Practicalities
- Templates can inline aggressively and remove abstraction overhead; they can also increase code size (one copy per specialisation). Consider type erasure or explicit instantiation if bloat matters.
- Keep APIs focused; don’t template “just because”. Use concepts to encode the real requirements (not “any type”).
Programming Insight (AI) — Code size & instantiation plan
- Ask AI to list heavy templates and suggest where explicit instantiation or type erasure reduces binary size.
10) Worked Example — A Constrained Time Sum
We combine your original Time idea with clean invariants and a template that accepts “minute-like” values.
#include <concepts>
#include <iostream>
struct Time {
int hours{};
int minutes{}; // invariant: 0 ≤ minutes < 60
void normalise() {
if (minutes >= 60) { hours += minutes / 60; minutes %= 60; }
if (minutes < 0) { int b = (-minutes + 59)/60; hours -= b; minutes += 60*b; }
}
};
// Accept any type convertible to minutes (e.g., int, long, etc.)
template <class M>
requires std::convertible_to<M,int>
Time add_minutes(Time t, M deltaMinutes) {
t.minutes += static_cast<int>(deltaMinutes);
t.normalise();
return t;
}
// Non-member + for Time + Time (symmetry)
inline Time operator+(Time a, const Time& b) { a.hours += b.hours; a.minutes += b.minutes; a.normalise(); return a; }
inline std::ostream& operator<<(std::ostream& os, const Time& t) {
return os << t.hours << "h " << t.minutes << "m";
}
This shows how templates (with concepts) let you accept a family of “minute-like” types while maintaining correctness and clarity.
11) Common Pitfalls (and fixes)
- Definitions not visible: put template definitions in headers or explicitly instantiate — avoid “undefined reference”.
- Unconstrained templates: lead to surprising overload resolution and unreadable errors. Add
requires. - Ambiguous overloads with mixed types: guide resolution via return type traits,
common_type, or separate overloads. - Over-general interfaces: template everything → hard to test and maintain. Start narrow; generalise only with need.
- Manual memory in templates: prefer RAII containers; avoid raw
new/deletein generic code.
12) Mini Exercises
- Constrain it: Write
dot(a,b)for containers of arithmetic types usingstd::rangesand concepts; ensure sizes match at runtime. - Partial specialisation: Create
as_string<T>that formats values; specialise forconst char*to avoid printing pointer values. - NTTP: Implement
Matrix<T, R, C>withoperator()indexing; addmultiplywith arequires(C1 == R2)constraint. - Error clarity: Replace an
enable_ifdesign withrequires; compare compiler diagnostics before/after.
Programming Insight (AI) — Check my templates
- Paste solutions; AI will validate constraints, recommend clearer concepts, and flag ODR/linker risks.
Summary Checklist
- Templates are blueprints; only instantiations are compiled. Type checking occurs at use sites.
- Constrain with concepts so errors are early and readable; avoid unconstrained “catch-all” templates.
- Prefer RAII and the Rule of Zero in generic code; don’t leak ownership complexity into templates.
- Put definitions where the compiler can see them (headers) or use explicit instantiation.
- When mixing types, define the result type policy (
common_type, concepts) explicitly. - Specialise deliberately; prefer overloads/concepts before partial specialisation when possible.
Advanced perspective: generality must be earned at the boundary
A template earns its generality only when every admitted argument preserves the same relationship
A template defines a family of declarations. It is not one function or class that changes type at runtime, and it is not untyped text pasted into a program. Template arguments determine which member of the family is considered, constraints decide whether that use is admitted, and the resulting specialisation is checked as C++.
The tempting measure of success is that one definition compiles for several types. That proves reuse of syntax. It does not prove reuse of meaning. If an operation called add truncates one type, overflows another and has no sensible interpretation for a third, increasing the number of accepted types has made the interface broader without making it better.
Trace the preserved program before claiming that it is generic
#include <concepts>
#include <iostream>
template<std::integral T>
T add(T left, T right) {
return left + right;
}
int main() {
std::cout << add(4, 7) << '\n';
std::cout << add(20L, 22L) << '\n';
}
The first call presents two int arguments, so deduction selects T = int. The associated constraint is satisfied, the program can use the add<int> specialisation, and the call prints 11. The second call presents two long arguments. It uses add<long> and prints 42. Nothing in either call asks a runtime object which implementation it prefers. The types have already settled the relationship during compilation.
| Call | Deduced argument | Specialisation used | Observed result |
|---|---|---|---|
add(4, 7) | T = int | add<int>(int, int) | 11 |
add(20L, 22L) | T = long | add<long>(long, long) | 42 |
add(4, 22L) | Conflicting deductions for the single T. | None from this call. | The call is rejected. |
The mixed call is important. The compiler does not silently invent a common template argument merely because int can convert to long. This template asks both function parameters to establish the same T, and deduction finds different answers. An explicit add<long>(4, 22L) would state a conversion policy visibly. A two-parameter template could state another policy. The interface must choose; the word generic cannot choose on its behalf.
Deduction, viability, selection and instantiation answer different questions
A long diagnostic becomes less mysterious when the compilation decisions are separated. The precise language rules contain qualifications, but the following trace is a useful account of these calls:
| Stage | Question | Possible outcome |
|---|---|---|
| Name lookup | Which declarations named add are visible? | A set of ordinary functions and function templates. |
| Argument deduction | Can template arguments be inferred from the call? | A candidate specialisation, or deduction failure. |
| Substitution and constraints | Is the candidate well-formed at its boundary and are its associated constraints satisfied? | The candidate remains viable or is removed. |
| Overload resolution | Which viable candidate is the best match? | One selected function, ambiguity or no match. |
| Instantiation when required | Is a definition needed for the selected specialisation? | The relevant definition is used to form and check it. |
| Program evidence | Does the selected operation produce the promised result? | A testable value, state change or failure. |
Failure at one stage does not justify changing another at random. If deduction conflicts, removing a concept does not repair deduction. If a constraint fails, adding a cast inside the function body cannot make the candidate satisfy that constraint. If two viable overloads are ambiguous, proving that either body would compile does not select between them.
A constraint states what may enter; it does not establish what the operation means
std::integral is a precise type-category requirement. It admits the language's integral types, which include Boolean and character types as well as the familiar signed and unsigned integer types. Consequently, add(true, true) satisfies this constraint: built-in addition produces an int, which is converted back to the declared bool result. The call compiles and returns true. Whether that deserves to be called addition is a design question the concept does not answer.
The same boundary does not prevent signed overflow, guarantee that a result is representable, reconcile signed and unsigned arithmetic or prove algebraic laws. Concepts and requires-expressions can state operations and type relationships that the compiler can check. The behavioural contract must still explain what valid inputs mean and what result the caller is entitled to expect.
| Claim | Compiler evidence available | What remains for the design |
|---|---|---|
| The argument is integral. | std::integral<T> is satisfied. | Whether every admitted integral type belongs to this domain. |
| An expression is available. | A requires-expression can check that a + b is well-formed. | Whether the expression has the intended meaning. |
| A result has a required type relationship. | A compound requirement can constrain the expression result. | Range, precision, overflow and failure policy. |
| An operation obeys a law. | Selected examples can be compiled and tested. | Evidence for identity, ordering, associativity or other domain laws. |
Template checking occurs at more than one point
A template definition is parsed when it is declared, and non-dependent names and constructs can be checked in that definition context. Other constructs depend on template parameters. Their meaning can only be completed after substitution for a particular specialisation. It is therefore too broad to say either that the template is fully checked when first seen or that no checking occurs until use.
This distinction matters when reading an error. A misspelled visible function name may be diagnosed while the template is defined. An expression such as value.process(), where the validity depends on T, may fail for a particular attempted specialisation. The location of the diagnostic tells you where the compiler discovered the problem; the dependency tells you why it could or could not decide earlier.
Choose parameters that expose the real variation
A type parameter is appropriate when an algorithm retains one relationship across a family of types. A constant template parameter records a compile-time value, such as the extent in Fixed<T, N>. A template template parameter accepts another template. These are not three ways to make code look more advanced. Each exposes a different decision to the caller and creates a different family of specialisations.
| Variation | Candidate mechanism | Boundary question |
|---|---|---|
| Element or value type | Type template parameter. | Which operations and semantics must every admitted type provide? |
| Compile-time extent or policy value | Constant template parameter. | Must the value affect the type or generated implementation? |
| Container or policy family | Template template parameter. | Is accepting a family clearer than accepting an object or callable? |
| Runtime choice | Ordinary function parameter or dynamic abstraction. | Would making a new compile-time type merely multiply specialisations? |
Class templates add another useful qualification. Naming a class specialisation does not necessarily instantiate every member definition immediately. The language instantiates what the context requires. A member that would be invalid for one T may remain irrelevant until that member is used, although the class boundary and any constraints still need coherent design.
Definition visibility is a reachability rule, not a header superstition
An implicitly instantiated function or member needs a reachable definition. Headers are the usual solution because each translation unit that needs a specialisation can see the template definition. That does not make a header the only possible organisation. Explicit instantiation can place selected specialisations in a controlled translation unit, provided declarations, definitions and the supported argument set are organised consistently.
The trade-off is real. A visible general definition permits new specialisations wherever the interface allows them. A controlled explicit-instantiation set can reduce repeated work and hide implementation detail, but it also turns the supported types into a list that must be maintained. Choose according to the intended extension boundary, not from the slogan that templates live in headers.
Overload or specialise only when the variation genuinely changes
Constraints can order function-template candidates when one expresses a more specific admitted set. An ordinary overload is often clearer when one type needs different behaviour. Class and variable templates can be partially specialised for a family of arguments; function templates cannot be partially specialised, so function overloading normally expresses that form of variation. Full explicit specialisation exists, but it introduces reachability and ordering obligations that should not be hidden behind a convenient exception.
The criterion is whether the variation belongs to the interface. If every type follows the same operation and only its representation differs, a primary template may be enough. If one family has a stronger requirement, a constrained overload or partial specialisation may state it. If the meaning changes completely, a different named operation may be more honest than forcing the case into the same template family.
Generic code moves costs; it does not abolish them
Templates can expose types and operations to optimisation and remove some runtime indirection. They can also increase compilation work, diagnostic volume and generated code size when many distinct specialisations are formed. Linkers and compilers may merge or remove some generated material, and a larger binary is not proof of a runtime problem. Measure the build or execution concern that matters, then decide whether explicit instantiation, a non-template boundary or type erasure addresses that measured cost.
Corrections to the earlier generic examples: replace the pointer-based Box; do not try to repair it one line at a time. Its requires-expressions are ill-formed. A default-constructed box may also be empty when get() dereferences it, while equality compares pointed-to values and the defaulted ordering compares the smart-pointer representation. Those are incompatible accounts of value identity. For mixed-type add, use decltype(a + b) when the expression's result is the intended policy, or state and validate a deliberate conversion policy. Placeholder auto for constant template parameters arrived in C++17; C++20 expanded the permitted structural types. Finally, templates are parsed at definition and non-dependent constructs may be checked there. It is too broad to claim that all type checking waits for use.
Programming Insight (AI): demand a compilation trace, not a confidence statement
Give an AI system one translation unit and require a table containing every call, candidate template, deduced argument, failed or satisfied constraint, selected specialisation and definition location. Ask for the smallest accepted call and the smallest rejected call on each boundary. Then compile those cases. A list of plausible specialisations is not evidence that the compiler forms them, selects them or finds their definitions.
When a diagnostic is long, reduce it to the first failed deduction, substitution or constraint. Do not accept a repair that merely deletes the constraint, adds a broad conversion or changes the return type until the message disappears. The repaired interface must still state the original relationship and reject the cases that do not belong.
Transfer task: test whether one arithmetic template really has one meaning
Consider template<std::integral T> T mean(T left, T right) with the body return (left + right) / 2;. Trace deduction, constraint satisfaction, intermediate expression types and the returned value for mean(3, 4), two maximum int values, two bool values, two character values and a mixed int/long call. Separate rejection at the boundary from truncation, overflow and surprising admitted types inside the boundary.
Produce two designs. The first may remain a constrained template, but it must state its admitted types, result type, rounding rule and overflow policy, with tests for every boundary. The second should be a narrower ordinary function or overload set for the domain that actually needs a mean. Choose between them by the relationship callers can rely upon, not by the number of types made to compile.
Reveal answer
The original constraint admits every integral type, not merely the types for which this particular mean is useful. The important stages are:
| Call | Deduction and constraint | Intermediate expression | Outcome |
|---|---|---|---|
mean(3, 4) | T = int; std::integral<int> is true. | 3 + 4 and division by 2 both have type int. | 7 is truncated by integer division to 3, which is returned as int. |
mean(INT_MAX, INT_MAX) | T = int; the constraint is true. | The addition is performed as signed int before division. | The addition overflows, so behaviour is undefined. There is no valid returned value to analyse. |
mean(true, true) | T = bool; std::integral<bool> is true. | Both operands undergo integral promotion; 2 / 2 has type int. | The integer 1 converts back to bool, giving true. |
mean(false, true) | T = bool; the constraint is true. | 1 / 2 is integer zero. | Zero converts back to false. The call compiles, but its meaning as a mean is doubtful. |
mean('0', '2') | T = char; std::integral<char> is true. | The characters promote to int. Decimal digit codes are contiguous, so their integer mean is the code for '1'. | The integer result converts back to char, giving '1'. Character arithmetic was admitted even though the constraint never said it was intended. |
mean(3, 4L) | The first argument suggests T = int; the second suggests T = long. | No single T is deduced, so no function body is formed. | The call is rejected at deduction. This is not truncation or overflow inside the function. |
Two other Boolean combinations follow the same promotions: false, false returns false, while true, false also truncates to false. Other character pairs follow their promoted integer values and convert the averaged result back to the deduced character type.
Design one: a deliberately integral mean
This version admits equal argument types drawn from the ordinary signed and unsigned integer types, while excluding Boolean and character types. It returns that same type. For an odd mathematical sum, std::midpoint rounds towards the first argument, and its integer implementation avoids overflow.
#include <concepts>
#include <numeric>
#include <type_traits>
template<class T>
concept MeanInteger =
std::integral<T>
&& !std::same_as<std::remove_cv_t<T>, bool>
&& !std::same_as<std::remove_cv_t<T>, char>
&& !std::same_as<std::remove_cv_t<T>, signed char>
&& !std::same_as<std::remove_cv_t<T>, unsigned char>
&& !std::same_as<std::remove_cv_t<T>, wchar_t>
&& !std::same_as<std::remove_cv_t<T>, char8_t>
&& !std::same_as<std::remove_cv_t<T>, char16_t>
&& !std::same_as<std::remove_cv_t<T>, char32_t>;
template<MeanInteger T>
T mean(T left, T right) noexcept
{
return std::midpoint(left, right);
}
The boundary is now testable: ordinary integers of one type compile; Boolean and character calls do not; a mixed int/long call does not silently select a conversion policy. Tests should include equal values, adjacent values in both argument orders, negative odd totals, zero, the minimum and maximum values, two maxima, and opposite extremes. The reversed adjacent-value test is necessary because rounding towards the first argument is part of this contract.
Design two: name the domain and remove unused generality
If the actual requirement is the mean of two assessment marks from 0 to 100, an ordinary function states more and needs less machinery:
#include <stdexcept>
int mean_mark(int left, int right)
{
if (left < 0 || left > 100 || right < 0 || right > 100) {
throw std::out_of_range{"mark must be in [0, 100]"};
}
return (left + right) / 2; // round down for a non-negative odd total
}
Here the sum cannot overflow, the result is an int, and the rounding rule is floor for the permitted non-negative inputs. Tests cover 0 and 100, equal marks, an even total, an odd total and every rejected endpoint. Choose the template only when callers genuinely need its whole admitted family and accept its first-argument rounding rule. Choose the ordinary function when the domain supplies the real bounds and meaning. Making bool compile is not useful generality.
A useful template preserves one named relationship while types or compile-time values vary. Deduction and constraints can police the entrance to that family. They cannot decide the family should exist. Generalise only after the invariant operation, the admitted variation and the evidence for both are clear.