58 / 163 · C++20 · 10 min
Concepts and requires: Constraining Callable Interfaces
C++20 concepts put the type properties and expression conditions a template needs onto the interface. A requires expression checks whether a valid operation exists; a requires clause controls candidate viability. They improve diagnostics and overload selection, but they cannot prove that runtime input or business semantics are correct.
In this lesson
Keep expression checks and candidate constraints separate
A concept is a named compile-time constraint; for example, the standard library's integral represents the integral-type property. A requires expression produces a bool and can check whether some expression is valid, whether a nested type exists, or whether a result satisfies another concept. A requires clause attaches those conditions to a template declaration so that a candidate is not viable when the constraint is unsatisfied.
Therefore in requires requires(T x) { x.size(); } the two keywords have different jobs: the first introduces the clause, the second introduces the expression. The repetition itself is not a syntax error, but extracting common conditions into a clearly named concept is usually better for reuse, diagnostics, and correctly establishing relationships among constraints.
Check the form you will actually call
If the function body calls size on a const object, the concept should also use const T&; it is not enough to prove that a non-const object has a function of the same name. A compound requirement { expression } -> convertible_to<size_t> checks convertibility of the result type; adding noexcept can also require that the expression does not throw.
The example HasSize shares the same call form as the function body: vector satisfies the constraint, int does not. Parameters inside requires are only checking tokens; they do not actually create a T, and they do not call size at runtime. Constraints can expose failure earlier, but they do not insert a hidden piece of runtime checking code.
Syntactic satisfaction is not semantic satisfaction
The compiler can decide whether a comparison expression is well-formed, but it cannot in general prove that a user comparator is transitive, or that a given size implementation truly returns the number of elements. When a standard concept has semantic requirements, the program author must still obey them; that an expression compiles only means it passed the mechanically checkable part.
When designing constraints, require only the capabilities the algorithm actually needs; do not force every caller to use a particular concrete container. A more constrained overload also does not necessarily win just because it has more condition text; atomic constraints and subsumption have strict rules. Prefer reusing named concepts rather than copying, in several places, a set of conditions that look equivalent but come from different sources.
Pitfalls
- requires does not validate runtime values; for example, integral cannot guarantee a divisor is nonzero, an index is in bounds, or integer addition does not overflow.
- Do not assume that putting any invalid expression into requires merely returns false; in non-template and other inapplicable contexts it can still be a hard compile error.
Run an example
Minimum C++20 · complete program · Download .cpp
#include <cassert>
#include <concepts>
#include <cstddef>
#include <iostream>
#include <vector>
template<class T>
concept HasSize = requires(const T& value) {
{ value.size() } -> std::convertible_to<std::size_t>;
};
template<HasSize T>
std::size_t count(const T& value) { return value.size(); }
int main() {
static_assert(HasSize<std::vector<int>>);
static_assert(!HasSize<int>);
std::vector<int> values{2, 4, 6};
assert(count(values) == 3);
std::cout << count(values) << '\n';
}
Compile locally
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread modern-concepts.cpp -o example && ./exampleExpected result
3
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If count's contract requires that the size call never throws, how should the concept be modified? Why is it not enough to casually add noexcept to count?
Show a reference answer
Change the compound requirement to { value.size() } noexcept -> std::convertible_to<std::size_t>; so that types that do not meet the requirement are excluded at the candidate stage. Adding noexcept only to count does not make the inner operation non-throwing; if an exception actually escapes, terminate is called. If a result conversion is also involved, a complete noexcept contract should check that conversion as well.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.