Generic Programming · #10 of 13

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:

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;
}
idle

A concept is `template <class T> concept Name = ...;`. The body is a *constraint expression* — usually `requires(...) { ... }`, sometimes a logical combination of other concepts.

expected output
1 2 3 4 
alpha beta gamma 
Or run locally
g++ -std=c++23 -O2 snippet.cpp && ./a.out

Three things to notice:

  1. Streamable is a named predicate. Anywhere you’d otherwise write “T must support << to an ostream,” you write Streamable<T>. It’s a first-class compile-time concept.
  2. 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.
  3. { os << v } -> std::same_as<std::ostream&> is a compound requirement: the expression os << v must compile, and its type must satisfy std::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:

ConceptMeans
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;
}
idle

The compiler picks the most-constrained matching overload. `bool` is `integral` *and* matches the non-template overload — non-templates win ties.

expected output
[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;
}
idle

`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.

expected output
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:

They’re noise when:

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

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.