CodeRigor

FREE LESSON

Templates and concepts

Write generic code that says what it needs: templates, deduction, concepts and requires-clauses, folds, and C++26 pack indexing.

A template is a recipe the compiler instantiates for each set of types you use it with. Before C++20, a template accepted any type and failed somewhere deep inside its body when a type didn't fit. Concepts let a template say up front what it needs, so wrong uses are rejected at the call site with a readable message — and so overloads can be chosen by what a type can do.

Template definitions normally belong in headers so each TU can instantiate them. Those definitions must still satisfy the One Definition Rule, including matching tokens after preprocessing and matching name lookup. A full function-template specialization is not implicitly inline merely because the primary is a template: define it in one .cpp or explicitly make its header definition inline. The project-building lesson shows the header and multi-TU rules in detail.

1. Function templates and deduction

C++
#include <print>
#include <string>

template <class T>
T twice(T x) {
    return x + x;
}

int main() {
    std::println("{}", twice(21));                   // T = int
    std::println("{}", twice(1.25));                 // T = double
    std::println("{}", twice(std::string("ab")));    // T = std::string
    std::println("{}", twice<double>(3));            // explicit: T = double
}
One template, one function per T
Each distinct T the calls deduce (or name) stamps out a separate function. twice<double>(3) reuses the double instantiation.
One template, one function per T
Each distinct T the calls deduce (or name) stamps out a separate function. twice<double>(3) reuses the double instantiation.

Deduction matches the argument's type against the parameter, and for a by-value parameter T it ignores two things: references and top-level const. Passing a const int& deduces T = int — you get a fresh copy, and that copy isn't const.

Parameter Argument const int& r Deduced T
T x copy int
T& x binds to it const int
const T& x binds to it int
T&& x (forwarding) lvalue const int&

Deduction also never converts: std::max(1, 2.0) fails because T can't be both int and double. Either convert an argument or name T explicitly.

2. Class templates and non-type parameters

Class templates work the same way; since C++17 their arguments can usually be deduced from constructors (CTAD: std::pair p{1, 2.5};). Template parameters can also be values — integers, enums, pointers, and in C++20 even structural class types:

C++
#include <array>
#include <cstddef>
#include <print>

template <class T, std::size_t N>
struct Ring {
    std::array<T, N> slots{};
    std::size_t head = 0;
    void push(T x) { slots[head++ % N] = x; }
};

template <std::size_t N>
constexpr std::size_t kib = N * 1024;

int main() {
    Ring<int, 3> r;   // N is part of the type: Ring<int, 3> != Ring<int, 4>
    for (int i = 1; i <= 5; ++i) r.push(i);
    std::println("{} {}", r.slots, kib<4>);
}

Overload, don't specialise, function templates. A full specialisation (template <> void f<int>(int)) doesn't take part in overload resolution — it only replaces the body if the primary template was chosen, which surprises everyone. A plain overload void f(int) is what you want. Specialising class templates is fine (std::hash<MyType>).

3. Concepts: saying what you need

A concept is a named compile-time predicate on types. Put one in front of a template parameter (or in a requires clause) and the template only exists for types that satisfy it:

C++
#include <concepts>
#include <print>

template <std::integral T>          // T must be an integer type
T gcd(T a, T b) {
    while (b != 0) {
        T t = a % b;
        a = b;
        b = t;
    }
    return a;
}

// The same constraint, three spellings:
template <class T> requires std::integral<T> T half_a(T x) { return x / 2; }
template <class T> T half_b(T x) requires std::integral<T> { return x / 2; }
std::integral auto half_c(std::integral auto x) { return x / 2; }   // abbreviated

int main() {
    std::println("{} {} {} {}", gcd(12, 18), half_a(9), half_b(9L), half_c(9u));
}
A concept is checked at the call site
The constraint is tested before the body is ever instantiated, so a wrong type is rejected with the concept's name — not with an error from deep inside gcd.
A concept is checked at the call site
The constraint is tested before the body is ever instantiated, so a wrong type is rejected with the concept's name — not with an error from deep inside gcd.

Call gcd(1.5, 2.5) and the error is one line: constraints not satisfied: std::integral<double> is false. Without the concept you'd get an error about % on doubles from inside the body — or worse, a template that compiles and does something silly.

C++
#include <concepts>

template <std::integral T>
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }

int main() { return static_cast<int>(gcd(1.5, 2.5)); }

Useful standard concepts

Concept Means
std::integral, std::signed_integral, std::floating_point arithmetic categories (integral includes bool and char!)
std::same_as<A, B>, std::convertible_to<From, To>, std::derived_from<D, B> type relationships
std::equality_comparable, std::totally_ordered == / < <= > >= all work
std::copyable, std::movable, std::regular value-type behaviour
std::invocable<F, Args...>, std::predicate<F, Args...> callables
std::ranges::range, forward_range, sized_range, random_access_range ranges
std::formattable<T, char> (C++23) usable with std::format

A concept has semantic promises too

Core Guidelines T.20–T.22 distinguish a useful concept from a bag of expressions that happen to compile. A comparison concept also has meaning: an ordering must obey its required laws. A requires-expression can check that a call exists and its result type fits; it cannot generally prove those laws for all inputs. A comparator implemented as a <= b compiles, but it is not a strict ordering because it reports a value less than itself. Document the semantic promise and test representative properties without calling a finite test a proof for every input. Prefer the appropriate standard concept when it fits, and require only the operations your algorithm actually needs.

4. Writing your own: requires-expressions

A requires-expression lists operations that must be valid. It's true or false, and never a compile error by itself — that's what lets concepts probe types:

C++
#include <concepts>
#include <cstddef>
#include <list>
#include <print>
#include <string>
#include <vector>

template <class C>
concept HasSize = requires(const C& c) {
    c.size();                                       // simple: must compile
    typename C::value_type;                         // type: must name a type
    { c.size() } -> std::convertible_to<std::size_t>;  // compound: and its type
    requires sizeof(typename C::value_type) <= 64;  // nested: must be true
};

int main() {
    std::println("{} {} {}", HasSize<std::vector<int>>, HasSize<std::list<std::string>>, HasSize<int>);
}

The four kinds: simple (expr;), type (typename X;), compound ({ expr } -> concept;, optionally noexcept), and nested (requires bool-constant;). Combine concepts with && and ||.

5. Subsumption: the most constrained overload wins

When several constrained overloads match, the compiler picks the one whose constraints include the others':

C++
#include <concepts>
#include <print>
#include <string_view>

template <std::integral T>        std::string_view kind(T) { return "integer"; }
template <std::signed_integral T> std::string_view kind(T) { return "signed integer"; }
template <class T>                std::string_view kind(T) { return "something else"; }

int main() {
    std::println("{} / {} / {}", kind(-1), kind(1u), kind(2.0));
}
kind(-1): two overloads match, the narrower one wins
signed_integral is defined in terms of integral, so it subsumes it: every type it accepts, integral accepts too. For int both match and the more constrained one is chosen; 1u only matches integral; 2.0 falls to the unconstrained one.
kind(-1): two overloads match, the narrower one wins
signed_integral is defined in terms of integral, so it subsumes it: every type it accepts, integral accepts too. For int both match and the more constrained one is chosen; 1u only matches integral; 2.0 falls to the unconstrained one.

std::signed_integral<T> is defined as std::integral<T> && std::is_signed_v<T>, so it subsumes std::integral<T> and wins for int. This only works through named concepts: constraints written with raw type traits are never recognised as the same, so these two overloads are ambiguous for int:

C++
#include <type_traits>

template <class T> requires std::is_integral_v<T> int f(T) { return 1; }
template <class T> requires std::is_integral_v<T> && std::is_signed_v<T> int f(T) { return 2; }

int main() { return f(1); }

6. if constexpr

Inside one template, if constexpr discards the branch that doesn't apply — the discarded branch isn't instantiated, so it may contain code that wouldn't compile for this T:

C++
#include <concepts>
#include <print>
#include <string>

template <class T>
auto describe(const T& x) {
    if constexpr (std::integral<T>)
        return x + 1;          // only instantiated for integers
    else
        return x.size();       // only instantiated for everything else
}

int main() {
    std::println("{} {}", describe(41), describe(std::string("hello")));
}

With a plain if, both branches are compiled for every T, and describe(41) fails on x.size().

7. Variadic templates and fold expressions

A pack Ts... holds zero or more types; xs... the matching values. A fold expression applies an operator across the pack:

Fold Expands to
(xs + ...) x1 + (x2 + (x3)) — right fold
(... + xs) ((x1) + x2) + x3 — left fold
(xs + ... + 0) with an initial value, so the empty pack works
(f(xs), ...) calls f on each, left to right (comma operator)
C++
#include <print>

template <class... Ts>
auto sum(Ts... xs) {
    return (xs + ... + 0);
}

template <class... Ts>
void print_all(const Ts&... xs) {
    ((std::print("{} ", xs)), ...);   // in order
    std::println("({} values)", sizeof...(xs));
}

int main() {
    std::println("{}", sum(1, 2, 3.5));
    print_all(1, "two", 3.0);
    print_all();
}
Folding 10, 4, 3 with −
A right fold nests to the right, a left fold to the left. For + it doesn't matter; for − it's 9 versus 3.
Folding 10, 4, 3 with −
A right fold nests to the right, a left fold to the left. For + it doesn't matter; for − it's 9 versus 3.

Order matters for non-associative operators: with 10, 4, 3, (xs - ...) is 10 - (4 - 3) = 9, while (... - xs) is (10 - 4) - 3 = 3.

8. C++26: pack indexing

Getting the nth element of a pack used to need std::tuple tricks. C++26 lets you index packs directly — types with Ts...[I], values with xs...[I]:

C++
#include <concepts>
#include <print>

template <class... Ts>
using first_t = Ts...[0];

template <class... Ts>
auto& last_of(Ts&... xs) {
    return xs...[sizeof...(xs) - 1];
}

int main() {
    static_assert(std::same_as<first_t<int, double, char>, int>);
    int a = 1, b = 2, c = 3;
    last_of(a, b, c) = 30;
    std::println("{} {} {}", a, b, c);
}

The index must be a constant expression in range; out of range is a compile error, not undefined behaviour.

9. Worked example: a constrained clamp_all

Clamp every element of a container into [lo, hi]. What does it need? The elements must be comparable with <, and the container must let us modify elements in place. Say exactly that:

C++
#include <concepts>
#include <cstddef>
#include <print>
#include <ranges>
#include <vector>

template <std::ranges::forward_range R>
    requires std::totally_ordered<std::ranges::range_value_t<R>>
std::size_t clamp_all(R& r, const std::ranges::range_value_t<R>& lo, const std::ranges::range_value_t<R>& hi) {
    std::size_t changed = 0;
    for (auto& x : r) {            // reference: modify the elements themselves
        if (x < lo)       { x = lo; ++changed; }
        else if (hi < x)  { x = hi; ++changed; }
    }
    return changed;
}

int main() {
    std::vector<int> v{-5, 3, 12};
    std::println("{} changed: {}", clamp_all(v, 0, 10), v);
}

Note hi < x rather than x > hi: the constraint guarantees both, but writing everything in terms of < is a good habit for generic code.

Faded example — your turn

Write a concept Shape for types with a const member area() returning something convertible to double, and a name() returning something convertible to std::string_view. Then use it to constrain total_area.

C++
template <class S>
concept Shape = requires(const S& s) {
    /* ??? */
};

template <std::ranges::range R>
    requires Shape<std::ranges::range_value_t<R>>
double total_area(const R& shapes);
Answer
C++
template <class S>
concept Shape = requires(const S& s) {
    { s.area() } -> std::convertible_to<double>;
    { s.name() } -> std::convertible_to<std::string_view>;
};

const S& s in the parameter list means the requirements are checked on a const object — so area() and name() must be const member functions.

Summary

  • Deduction for T x drops references and top-level const; it never converts.
  • Prefer overloading to specialising function templates.
  • Concepts constrain templates: template <std::integral T>, requires …, std::integral auto.
  • Write concepts with requires-expressions (simple, type, compound, nested). Inside a concept, failure means false, not an error.
  • The most constrained overload wins — but only through named concepts, not raw traits.
  • if constexpr discards the branch that doesn't apply.
  • Folds: (xs op ...), (... op xs), (f(xs), ...) for in-order side effects.
  • C++26 pack indexing: Ts...[I], xs...[I].

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Function and class templates, instantiation and argument deduction from the ground up.

Modern template code: auto parameters, variadic templates and concepts in one hour.

Concepts and requires-clauses: constraining templates so errors say what's missing.

Templates part 1: deduction, specialisation and the vocabulary used throughout this skill.

Templates part 2: variadic templates, folds and SFINAE vs concepts.

C++26 pack indexing (Ts...[I]) in four minutes.

requires clauses vs requires expressions — the two meanings of `requires` in constrained templates.

Rewrites SFINAE-based constraints as C++20 concepts, side by side.