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 constexprdiscards 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
.cppfile 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 form | Expansion | Example |
|---|---|---|
(... 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 pack | All-true check |
(f(vs), ...) | Comma fold for side effects | Print 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
Numericbeatsstd::integralif it is defined in terms of it. requiresclauses are checked before the body, so a function constrained out is simply not a candidate for overload resolution.- Test your concepts with
static_assertin 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.Related
Lambdas, algorithms and ranges References, const correctness and value categories
Last refreshed 2026-09-18.