Concepts + `requires`
Giving names to template constraints — the C++20 feature that made templates legible
Why it matters
The previous lesson’s templates accept any T. Pass an incompatible
type, and the error reports failure 47 lines deep inside an instantiation
chain. The standard library is full of “if your T is not Hashable, the
compiler will eventually notice when std::hash<T> is invoked from
inside std::unordered_map<T, V>::insert.”
Concepts, added in C++20, fix this. A concept is a named compile-time predicate on types. You write the constraint once at the declaration of a template, and the compiler:
- Rejects bad calls at the call site, with an error that names the unsatisfied concept.
- Picks the best-matching overload by which concepts each candidate satisfies — overload resolution gets a real say.
- Reads better. Code like
template <Sortable C> void sort(C&)documents itself.
Concepts are the single biggest improvement to generic C++ ergonomics since templates themselves.
A concept, defined and used
The minimal example. We want a generic print_all that works on any
container whose elements are streamable to std::cout:
#include <iostream>
#include <vector>
#include <list>
#include <concepts>
#include <string>
// A concept: "T can be sent to std::ostream via <<".
template <class T>
concept Streamable = requires(std::ostream& os, const T& v) {
{ os << v } -> std::same_as<std::ostream&>;
};
// A function template constrained on Streamable elements.
template <class C>
requires Streamable<typename C::value_type>
void print_all(const C& c) {
for (const auto& x : c) std::cout << x << ' ';
std::cout << "\n";
}
int main() {
std::vector<int> v{1, 2, 3, 4};
std::list<std::string> ls{"alpha", "beta", "gamma"};
print_all(v);
print_all(ls);
// std::vector<std::vector<int>> won't compile — the inner vector
// isn't Streamable. The error names the failing concept, not a
// 40-line substitution dump.
return 0;
}
A concept is `template <class T> concept Name = ...;`. The body is a *constraint expression* — usually `requires(...) { ... }`, sometimes a logical combination of other concepts.
1 2 3 4
alpha beta gamma Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out Three things to notice:
Streamableis a named predicate. Anywhere you’d otherwise write “T must support<<to an ostream,” you writeStreamable<T>. It’s a first-class compile-time concept.requires(std::ostream& os, const T& v) { ... }is a requires expression. It introduces fake parameters (they only exist for type-checking), then a body of requirement clauses the compiler must be able to type-check.{ os << v } -> std::same_as<std::ostream&>is a compound requirement: the expressionos << vmust compile, and its type must satisfystd::same_as<std::ostream&>. Both checks together.
The four shapes of requires
You’ll see requires in four different positions. They do four
different things.
// 1. requires-clause on a template declaration:
template <class T>
requires Streamable<T>
void print(const T& v);
// 2. requires-clause as a trailing form:
template <class T>
void print(const T& v) requires Streamable<T>;
// 3. requires expression — produces a bool, used INSIDE a concept or constraint:
template <class T>
concept HasSize = requires(T t) { t.size(); };
// 4. requires-clause inside a requires expression (yes, really):
template <class T>
concept Container = requires(T c) {
requires Streamable<typename T::value_type>; // nested requires
{ c.size() } -> std::convertible_to<std::size_t>;
};
The first two are constraints on a template. The third is the predicate
machinery that you build constraints from. The fourth is rare but useful
— it lets a concept depend on another concept’s satisfaction inside the
body of a requires expression.
The mental model: concept Name = expr; defines a named bool over
types. requires(args) { … } is one of the ways to compute that
bool, by asking “would these expressions compile?”
Built-in standard concepts
C++20 ships a vocabulary of named concepts in <concepts> and friends.
The ones you’ll reach for daily:
| Concept | Means |
|---|---|
std::same_as<T, U> | T and U are the same type |
std::derived_from<D, B> | D is B or derives from B |
std::convertible_to<F, T> | F implicitly converts to T |
std::integral<T> | T is an integer type |
std::floating_point<T> | T is a floating-point type |
std::equality_comparable<T> | T supports == and != |
std::totally_ordered<T> | T supports <, <=, >, >=, ==, != consistently |
std::regular<T> | T is default-constructible, copyable, equality-comparable — “behaves like an int” |
std::invocable<F, Args...> | F(args...) is a valid expression |
std::ranges::range<T> | T has begin() and end() |
std::ranges::sized_range<T> | T is a range plus size() in O(1) |
The standard library has another two dozen, mostly in <ranges> and
<iterator>. Spend an hour reading the list once. You’ll be amazed how
many of them say exactly what you mean.
Overload resolution with concepts
This is the killer feature. Different overloads, each with a different constraint, and the compiler picks the most-constrained one that matches:
#include <iostream>
#include <concepts>
#include <string>
// Generic fallback for anything streamable.
template <class T>
requires (!std::integral<T>)
void describe(const T& v) {
std::cout << "[other] " << v << "\n";
}
// A more specific overload for integers.
template <std::integral T>
void describe(T v) {
std::cout << "[integral] " << v << " (size " << sizeof(T) << ")\n";
}
// An even more specific overload for booleans.
void describe(bool b) {
std::cout << "[bool] " << (b ? "true" : "false") << "\n";
}
int main() {
describe(42);
describe(3.14);
describe(std::string{"hi"});
describe(true);
return 0;
}
The compiler picks the most-constrained matching overload. `bool` is `integral` *and* matches the non-template overload — non-templates win ties.
[integral] 42 (size 4)
[other] 3.14
[other] hi
[bool] true Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out The template <std::integral T> shorthand is a concept used as a
constrained-type-specifier. It’s syntactic sugar for template <class T> requires std::integral<T>. Use this shorter form whenever you can —
it’s the standard idiom now.
Defining your own concept
A few examples of building a concept out of requires:
// "T has a clear() method."
template <class T>
concept Clearable = requires(T& t) { t.clear(); };
// "T behaves like a 2D point (has .x and .y)."
template <class T>
concept Point2D = requires(T p) {
{ p.x } -> std::convertible_to<double>;
{ p.y } -> std::convertible_to<double>;
};
// "T is iterable and its elements are addable to each other."
template <class T>
concept SummableRange =
std::ranges::input_range<T> &&
requires (std::ranges::range_value_t<T> a, std::ranges::range_value_t<T> b) {
{ a + b } -> std::convertible_to<std::ranges::range_value_t<T>>;
};
&& and || combine concepts logically. The pattern: a concept is
either a logical expression over other concepts, or a requires
expression that describes what compiles.
auto as a constrained parameter
C++20 also lets you write constrained auto in function parameters
— a feature called abbreviated function templates:
// These two are equivalent:
template <std::integral T>
void f(T x) { /* … */ }
void f(std::integral auto x) { /* … */ }
The Concept auto form is the most readable for short signatures. The
template form is what you want when the function body or constraint
needs to refer to T by name.
auto without a constraint in a parameter position (still C++20+) is
also legal: void f(auto x). It’s an unconstrained template, which is
exactly what template <class T> void f(T x) was the long way. Use
constrained auto whenever you can — it reads cleaner.
A real example: a Sortable constraint
Putting it together. A reasonable definition of “C is sortable”:
#include <iostream>
#include <vector>
#include <list>
#include <ranges>
#include <algorithm>
#include <concepts>
template <class C>
concept Sortable =
std::ranges::random_access_range<C> &&
std::sortable<std::ranges::iterator_t<C>>;
// Now this signature documents exactly what it needs.
template <Sortable C>
void sort_and_print(C& c) {
std::ranges::sort(c);
for (const auto& x : c) std::cout << x << ' ';
std::cout << "\n";
}
int main() {
std::vector<int> v{3, 1, 4, 1, 5, 9, 2, 6};
sort_and_print(v);
// sort_and_print(std::list<int>{3, 1, 4}); // would fail: list is bidirectional, not random-access.
return 0;
}
`Sortable` is a one-line concept that combines two standard concepts. Pass a `std::list<int>`, and the compiler rejects it with 'list does not satisfy Sortable', not a substitution-failure dump.
1 1 2 3 4 5 6 9 Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out The standard library already defines std::sortable — the example
combines it with std::ranges::random_access_range to make the
requirement explicit. In real code you’d usually just use std::sortable.
When NOT to use a concept
Concepts add value when:
- The template is meant to be a library boundary.
- The constraint will be reused (write it once, use it in many signatures).
- The unconstrained version would produce surprising error messages.
They’re noise when:
- The template is a one-off implementation detail. Just
class Tand move on. - The constraint is “literally anything that compiles.” The unconstrained template already says that.
The rule: constrain the public surface, leave the internals unconstrained. The same rule library designers apply to type annotations in Python or to interface declarations in Go.
A note on SFINAE
Before C++20, you’d express constraints by SFINAE (Substitution Failure Is Not An Error): writing template overloads whose return type or parameter list was only valid for the types you wanted, so that substitution failure for the wrong type just removed the overload from consideration. The classic spelling:
template <class T>
std::enable_if_t<std::is_integral_v<T>, void>
f(T x);
This works, was widely used, and produces error messages that look like a thrown blender. Concepts replace it. If you see SFINAE in older code, the modern equivalent is almost always a concept-constrained template that reads ten times more clearly.
Key takeaways
- A concept is a named compile-time predicate on types:
template <class T> concept Name = ...;. requires(args) { … }is a requires expression — a compile-time bool that’s true when each statement in the body would type-check.- Use
template <Concept T> void f(T)or the abbreviatedvoid f(Concept auto x)to constrain a template parameter. - The standard library ships a vocabulary of concepts in
<concepts>,<iterator>,<ranges>. Reach for them before inventing your own. - Overload resolution honors constraints. The most-constrained matching overload wins. This replaces the SFINAE-based “overload removal” trick.
- Constrain library boundaries, leave internals unconstrained.
What’s next
Phase 5 — Concurrency. Lesson 11 introduces std::thread, std::async,
and the futures/promise model. Lesson 12 is the lesson most C++ courses
get wrong: atomics and the memory model. The capstone after that is
C2 — Route Planner with A* on OpenStreetMap data.