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
#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
}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:
#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:
#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));
}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.
#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:
#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':
#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));
}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:
#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:
#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) |
#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();
}+ 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]:
#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:
#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.
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
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 xdrops references and top-levelconst; 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 constexprdiscards 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].