Lesson 7 of 8
maximum(a, b) requires that T supports >. Nothing in the signature says so, so a type without
> produces an error deep inside the function body — a body the caller never wrote.
C++20 concepts let you state the requirement in the signature, where it belongs.
The problem, measured
Sorting a vector of a type with no <:
// #include <algorithm>
// #include <vector>
//
// struct Widget { int id; };
//
// int main() {
// std::vector<Widget> v(3);
// std::sort(v.begin(), v.end()); // Widget has no operator<
// return 0;
// }
GCC 13 produces 78 lines of diagnostics, beginning:
In file included from /usr/include/c++/13/bits/stl_algobase.h:71,
from /usr/include/c++/13/algorithm:60,
/usr/include/c++/13/bits/predefined_ops.h: In instantiation of
'constexpr bool __gnu_cxx::__ops::_Iter_less_iter::operator()(...)'
/usr/include/c++/13/bits/stl_algo.h:1819:14: required from
'constexpr void std::__insertion_sort(...)'
Nothing there mentions your code. You have to read to the bottom to find no match for operator<,
and then work out that the fix is to give Widget a comparison.
The same mistake with std::ranges::sort, which is constrained by concepts, produces 34 lines
and says the useful thing near the top:
error: no match for call to '(const std::ranges::__sort_fn) (std::vector<Widget>&)'
note: candidate: 'template<...> requires (random_access_iterator<_Iter>)
&& (sentinel_for<_Sent, _Iter>) && (sortable<_Iter, _Comp, _Proj>) ...'
sortable is the requirement that failed, and its name says what is missing. That difference —
half the output, and the requirement named rather than implied — is what concepts buy.
Writing one
#include <concepts>
#include <iostream>
#include <string>
// A concept names a requirement on a type.
template <typename T>
concept Comparable = requires(const T& a, const T& b) {
{ a > b } -> std::convertible_to<bool>;
};
// Three equivalent ways to constrain a template with it.
template <Comparable T>
T maximum(const T& a, const T& b) {
return (a > b) ? a : b;
}
template <typename T>
requires Comparable<T>
T maximum2(const T& a, const T& b) {
return (a > b) ? a : b;
}
auto maximum3(const Comparable auto& a, const Comparable auto& b) {
return (a > b) ? a : b;
}
int main() {
std::cout << maximum(3, 7) << "\n";
std::cout << maximum2(2.5, 1.5) << "\n";
std::cout << maximum3(std::string("apple"), std::string("banana")) << "\n";
return 0;
}
7
2.5
banana
All three forms mean the same thing. Which to use:
template <Comparable T>— shortest, and the usual choice.requires Comparable<T>— when the constraint is complex or combines several concepts.Comparable auto— the abbreviated form; shortest of all, at the cost of not namingT.
The requires(const T& a, const T& b) { ... } block introduces names for hypothetical objects and
lists expressions that must be valid for them. Nothing is evaluated — the compiler only checks
whether each expression would compile.
{ a > b } -> std::convertible_to<bool> says the expression must be valid and its result must
convert to bool. Without the arrow part, a type whose operator> returned a Widget would
satisfy the concept.
The error, before and after
Calling maximum(Widget{1}, Widget{2}) unconstrained:
error: no match for 'operator>' (operand types are 'const Widget' and 'const Widget')
8 | return (a > b) ? a : b;
The error is at line 8 — inside maximum, which the caller did not write.
Constrained with Comparable:
error: no matching function for call to 'maximum(Widget&, Widget&)'
19 | std::cout << maximum(a, b).id << "\n";
note: candidate: 'template<class T> requires Comparable<T> T maximum(const T&, const T&)'
note: constraints not satisfied
note: required for the satisfaction of 'Comparable<T>' [with T = Widget]
note: the required expression '(a > b)' is invalid
Now the error is at line 19, the call, it names the constraint (Comparable), and the last
line says exactly which requirement failed. That is the difference in one comparison: the blame
moved from the library to the call site.
The standard concepts
<concepts> provides the common ones, so you rarely define your own:
| Concept | Means |
|---|---|
| std::integral<T> | an integer type |
| std::floating_point<T> | float, double, long double |
| std::same_as<T, U> | exactly the same type |
| std::convertible_to<From, To> | implicitly convertible |
| std::derived_from<D, B> | D inherits from B |
| std::equality_comparable<T> | supports == and != |
| std::totally_ordered<T> | supports <, >, <=, >= |
| std::copyable<T> | can be copied and assigned |
| std::movable<T> | can be moved |
| std::invocable<F, Args...> | callable with those arguments |
<iterator> and <ranges> add std::input_iterator, std::random_access_iterator,
std::range, std::sortable, and the rest of the machinery std::ranges is built on (lesson
13.9).
Combining and overloading
#include <concepts>
#include <iostream>
#include <string>
#include <vector>
// Overload resolution by concept: the most specific satisfied one wins.
template <typename T>
void describe(const T&) {
std::cout << "something\n";
}
template <std::integral T>
void describe(const T& v) {
std::cout << "an integer: " << v << "\n";
}
template <std::floating_point T>
void describe(const T& v) {
std::cout << "a real number: " << v << "\n";
}
// Combining concepts with && and ||.
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <typename T>
concept Printable = requires(const T& v) { std::cout << v; };
template <typename T>
concept PrintableNumber = Number<T> && Printable<T>;
template <PrintableNumber T>
void show(const T& v) { std::cout << "printable number: " << v << "\n"; }
// A requires block can demand members, types, and expressions.
template <typename T>
concept Container = requires(T c) {
typename T::value_type; // must have a nested type
c.size(); // must have size()
c.begin(); // must have begin()
c.end(); // must have end()
};
template <Container C>
void count_of(const C& c) {
std::cout << "container of " << c.size() << "\n";
}
int main() {
describe(42);
describe(3.5);
describe(std::string("text"));
show(7);
show(2.5);
std::vector<int> v = {1, 2, 3};
count_of(v);
count_of(std::string("hello"));
std::cout << std::boolalpha;
std::cout << "Container<std::vector<int>>: " << Container<std::vector<int>> << "\n";
std::cout << "Container<int>: " << Container<int> << "\n";
return 0;
}
an integer: 42
a real number: 3.5
something
printable number: 7
printable number: 2.5
container of 3
container of 5
Container<std::vector<int>>: true
Container<int>: false
Four things there are worth naming.
Overloading on constraints. Three describe overloads, and the compiler picks the most
constrained one that is satisfied. describe(42) gets the std::integral version rather than
the unconstrained one. This replaces the SFINAE tricks (std::enable_if) that older code uses for
the same purpose, and it is far more readable.
&& and ||. Concepts compose like boolean expressions. PrintableNumber is a name for a
combination, which keeps signatures short.
A requires block can demand more than expressions. typename T::value_type; requires a
nested type to exist. c.size(); requires the call to be valid. std::string satisfies
Container — it has all four — which is why count_of(std::string("hello")) prints 5.
A concept is usable as a bool. Container<std::vector<int>> is a compile-time constant, so
it can be printed, or used in if constexpr, or asserted with static_assert.
Constraining a class template
// template <std::integral T>
// class Counter {
// T value = 0;
// public:
// void increment() { ++value; }
// T get() const { return value; }
// };
//
// Counter<int> ok; // fine
// Counter<double> bad; // error at the declaration, naming std::integral
The failure is reported where the type is named, not where a member is first called. That is better than the "members instantiate only when called" rule from lesson 12.3, which can hide a mistake for a long time.
What concepts do not do
Two honest limits.
They do not check the body. A concept states requirements; nothing verifies that the function
only uses what it asked for. Ask for > and then also use +, and a type with > but no +
fails inside the body, exactly as before. Keeping the concept and the body in sync is your job.
They do not check semantics. std::totally_ordered<T> requires the operators to exist. It
cannot check that a < b and b < a are not both true. A type that satisfies the concept can
still break std::sort. The standard calls these semantic requirements and states them in
prose, unenforced.
Try it
Write concepts Addable, Printable, and Sortable (a range whose elements are totally
ordered). Use them to constrain a sum, a print_all, and a sort_and_print. Then call each
with a type that fails, and compare the error against the unconstrained version.
Checkpoint
- A concept names a requirement on a type, checked at the call rather than inside the body.
template <typename T> concept Name = requires(T a) { /* valid expressions */ };{ expr } -> std::convertible_to<R>constrains the expression's result type too.- Three equivalent spellings:
template <C T>,requires C<T>, andC auto. <concepts>providesintegral,floating_point,totally_ordered,copyable, and more.- Compose with
&&and||; a concept is a compile-timeboolyou can print or assert. - Overloads are ranked by how constrained they are — the modern replacement for
std::enable_if. - A
requiresblock can demand nested types, members, and expressions. - Concepts check syntax, not semantics, and do not verify that the body matches the constraint.
Questions about this lesson
Log in to ask a question.