Templates, concepts and generic programming

Write function and class templates, use variadic packs and fold expressions, and constrain them with C++20 concepts for readable errors.

Function and class templates

A template is a recipe the compiler instantiates per set of arguments. Because instantiation happens in the caller's translation unit, template definitions normally live in headers rather than in a .cpp file.

#include <vector>
#include <concepts>
#include <stdexcept>

// deduction: T comes from the arguments
template <class T>
T max_of(T a, T b) { return (a < b) ? b : a; }

// explicit specialisation is rare; prefer overloading or if constexpr
template <class T>
std::string describe(T v) {
    if constexpr (std::integral<T>)        return "integer " + std::to_string(v);
    else if constexpr (std::floating_point<T>) return "float " + std::to_string(v);
    else                                    return "other";
}

// a class template with a non-type parameter
template <class T, std::size_t N>
struct FixedArray {
    T data[N]{};
    constexpr std::size_t size() const noexcept { return N; }
    T& operator[](std::size_t i) { return data[i]; }
};

// CTAD: the class template argument is deduced from the constructor
template <class T>
struct Pair { T first, second; };
Pair p{1, 2};        // Pair<int>
  • Template code is compiled only when instantiated, so syntax errors surface at the call site. That is why unconstrained templates produce pages of diagnostics.
  • if constexpr discards the untaken branch at compile time, so the code in it need not even compile for that type.
  • Non-type template parameters must be constant expressions in C++17; C++20 relaxes this for literal class types.
  • Definition in a header is the default. Put a definition in a .cpp file only if you explicitly instantiate every type you use there.

Variadic templates and folds

#include <utility>
#include <iostream>
#include <string>

// C++17 fold expressions replace the old recursive base case
template <class... Ts>
auto sum_all(Ts... vs) { return (vs + ...); }            // unary right fold

template <class... Ts>
void print_all(const Ts&... vs) { ((std::cout << vs << ' '), ...); std::cout << '\n'; }

template <class... Ts>
bool all_true(Ts... vs) { return (... && vs); }          // unary left fold

// forwarding every argument through to a constructor
template <class T, class... Args>
T make(Args&&... args) { return T(std::forward<Args>(args)...); }

// count arguments at compile time
template <class... Ts>
constexpr std::size_t count() { return sizeof...(Ts); }

int main() {
    std::cout << sum_all(1, 2, 3, 4) << '\n';   // 10
    print_all("id", 7, 3.5);
    static_assert(count<int, double, char>() == 3);
}
Fold formExpansionExample
(... op pack)Left fold(... + vs)
(pack op ...)Right fold(vs + ...)
(init op ... op pack)Binary fold with seed(0 + ... + vs)
(... && vs)Logical AND over a packAll-true check
(f(vs), ...)Comma fold for side effectsPrint or call each element

An empty parameter pack makes a logical fold evaluate to the identity element (true for &&, false for ||), but an arithmetic fold over an empty pack is a compile error because there is no neutral value. Add a seed: (0 + ... + vs).

Concepts and requires clauses

#include <concepts>
#include <ranges>
#include <string>

// define a concept once, reuse it everywhere
template <class T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template <class T>
concept Printable = requires(T v, std::ostream& os) {
    { os << v } -> std::same_as<std::ostream&>;     // expression must be valid
    requires sizeof(v) > 0;                         // nested requirement
};

// three equivalent ways to constrain
template <Numeric T>            T twice_a(T v) { return v + v; }
template <class T> requires Numeric<T> T twice_b(T v) { return v + v; }
template <class T> T twice_c(T v) requires Numeric<T> { return v + v; }

// concept on a class template
template <Printable T>
struct Box { T value; };

// abbreviated function template syntax (C++20)
void shout(const Printable auto& v) { std::cout << v << "!\n"; }

int main() {
    static_assert(Numeric<int>);
    static_assert(!Numeric<std::string>);
    shout(42);
}
  • A concept turns an instantiation failure into a one-line message naming the unsatisfied constraint, instead of a hundred lines inside <type_traits>.
  • Overloads constrained by concepts are ordered by subsumption: a stricter concept wins, so Numeric beats std::integral if it is defined in terms of it.
  • requires clauses are checked before the body, so a function constrained out is simply not a candidate for overload resolution.
  • Test your concepts with static_assert in the same header. A concept that never rejects anything is not constraining anything.
⚠️
An unconstrained template accepts any type and fails deep inside its body. A constrained one fails at the call site with the constraint name. Adding concepts to an existing template library is usually the highest-value readability change available.

FAQ

Why do I get a linker error for a template?
The definition was in a .cpp file, so the compiler never saw it while instantiating for your type. Move the definition into the header, or add an explicit instantiation such as template class Stack<int>; in that file.
Should I use std::enable_if or concepts?
Concepts, for anything on C++20 or later. enable_if is still needed in code that must compile as C++17 or on older toolchains, and it produces far worse diagnostics.

Lambdas, algorithms and ranges References, const correctness and value categories

Last refreshed 2026-09-18.