Concepts and requires Expressions
C++20 concepts and requires expressions in depth — four requirement forms, concept definition, subsumption rules, and overload resolution.
requires expressionsince C++20A compile-time boolean expression that probes whether a set of expressions or types are syntactically well-formed, returning true if every requirement is satisfied and false otherwise without causing a hard error.
Overview
C++20 introduced two related constructs that share the requires keyword:
- requires clause — a constraint predicate gating template instantiation:
template<typename T> requires Predicate<T> - requires expression — a compile-time boolean that probes expression validity:
requires(T x) { x.begin(); }
A concept is a named, reusable constraint (typically a requires expression) bound to a template parameter with the concept keyword. Concepts compose, participate in subsumption-based overload resolution, and produce far better diagnostics than raw SFINAE ever could.
The double requires requires idiom is intentional and valid: the first is the clause, the second is the expression. This is a common source of confusion but not an error.
Syntax
Four Requirement Kinds Inside a requires Expression
// 1. Simple requirement — expression must be syntactically valid
requires (T a, T b) {
a + b; // T supports binary +
a.begin(); // T has a member begin()
T{}; // T is default-constructible
}
// 2. Type requirement — a nested type or template instantiation must be valid
requires {
typename T::value_type; // nested type must exist
typename T::allocator_type;
typename std::iterator_traits<T*>; // template instantiation must succeed
}
// 3. Compound requirement — expression valid + return type satisfies concept
requires (T a) {
{ a.size() } -> std::convertible_to<std::size_t>; // C++20
{ a.begin() } -> std::input_or_output_iterator; // C++20
{ a.clone() } noexcept -> std::same_as<T>; // noexcept + type
}
// 4. Nested requirement — an inner constraint must hold
requires (T a) {
requires std::is_trivially_copyable_v<T>;
requires sizeof(T) <= 16;
}All four kinds may appear freely mixed within a single requires expression.
Defining Concepts
// C++20
template<typename T>
concept Printable = requires(T x) {
{ std::cout << x } -> std::same_as<std::ostream&>;
};
template<typename T>
concept Container = requires(T c, const T cc) {
typename T::value_type;
typename T::iterator;
typename T::const_iterator;
{ c.begin() } -> std::same_as<typename T::iterator>;
{ cc.begin() } -> std::same_as<typename T::const_iterator>;
{ c.size() } -> std::convertible_to<std::size_t>;
{ c.empty() } -> std::convertible_to<bool>;
};
// Concepts compose with && and || directly
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept SizedContiguousRange =
std::ranges::contiguous_range<T> && // C++20
std::ranges::sized_range<T>; // C++20Applying Concepts — Four Equivalent Syntaxes
// 1. requires clause after template head — scales to complex multi-constraints
template<typename T>
requires Container<T>
void process(T& c);
// 2. Constrained template parameter
template<Container T>
void process(T& c);
// 3. Abbreviated function template (C++20)
void process(Container auto& c);
// 4. Trailing requires clause
template<typename T>
void process(T& c) requires Container<T>;Style 3 is the most concise for single-parameter functions. Style 1 is preferred when multiple type parameters carry separate constraints; keeping the requires clause on its own line makes it unambiguous and scannable.
Examples
Arithmetic Range Algorithm
#include <concepts>
#include <ranges>
#include <vector>
#include <list>
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>; // C++20
template<typename T>
concept ArithmeticRange =
std::ranges::input_range<T> &&
Arithmetic<std::ranges::range_value_t<T>>; // C++20
template<ArithmeticRange R>
auto range_sum(const R& r) -> std::ranges::range_value_t<R> {
using V = std::ranges::range_value_t<R>;
V total{};
for (const auto& v : r) total += v;
return total;
}
std::vector<double> v{1.5, 2.5, 3.0};
auto s = range_sum(v); // double — OK
std::list<int> l{1, 2, 3};
auto t = range_sum(l); // int — OK
range_sum(std::string{"hi"});
// Error: constraint not satisfied — char is not ArithmeticSubsumption-Driven Overload Resolution
Subsumption fires when one concept's constraints are a provable superset of another's — specifically when the normalized atomic constraints of concept A appear within concept B via logical conjunction. This enables the compiler to pick the more constrained overload without ambiguity.
// C++20
template<typename T>
concept Hashable = requires(T x) {
{ std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};
// OrderedHashable subsumes Hashable via conjunction
template<typename T>
concept OrderedHashable = Hashable<T> && std::totally_ordered<T>; // C++20
template<Hashable T>
void insert(T key) { /* hash-only container path */ }
template<OrderedHashable T>
void insert(T key) { /* can also binary-search; preferred when available */ }
insert(42); // OrderedHashable<int> — second overload wins
insert(std::string{"x"}); // OrderedHashable<std::string> — second overloadSubsumption checks atomic constraint identity, not semantic equivalence. Two independently named concepts with identical bodies do not subsume each other — the compiler cannot prove one implies the other without structural composition.
Constraining Class Templates and Non-Template Members
// C++20
template<typename T>
requires std::is_trivially_copyable_v<T>
class Ring {
T data_[64];
public:
// Non-template members can also carry requires clauses (C++20)
void push(T val) requires std::copyable<T>;
void push(T&& val) requires std::movable<T>;
T pop() requires std::default_initializable<T>;
};Ad-Hoc Inline Constraint (requires-requires)
Use the double form when the constraint is specific to one callsite and not worth naming:
// C++20
template<typename T>
requires requires(T& x, std::size_t n) { x.reserve(n); }
void preallocate(T& c, std::size_t n) {
c.reserve(n);
}
// If this constraint appears in more than one template, extract it:
template<typename T>
concept Reservable = requires(T& x, std::size_t n) { x.reserve(n); };Best Practices
Prefer named concepts over inline requires expressions for any constraint that is referenced more than once. Named concepts participate in subsumption; structurally identical but separately defined expressions do not.
Use compound requirements with the weakest satisfying concept for return types. -> std::convertible_to<bool> is more general and usually more correct than -> std::same_as<bool> for predicates like empty().
Validate your concept assumptions with static_assert before shipping:
static_assert(std::ranges::random_access_range<std::vector<int>>); // C++20
static_assert(!std::ranges::random_access_range<std::list<int>>); // C++20
static_assert(Container<std::vector<double>>);
static_assert(!Container<int>);Prefer requires clause on a separate line from the template head for multi-constraint templates. It visually separates the parameter declaration from the constraint, which aids code review.
Common Pitfalls
Renamed Concepts Break Subsumption
template<typename T>
concept A = std::integral<T>;
template<typename T>
concept B = std::integral<T>; // same body, different name
template<A T> void f(T);
template<B T> void f(T); // ambiguous — A and B do NOT subsume each otherThe compiler compares atomic constraint expressions by identity (same template, same arguments), not by logical equivalence. Subsumption is structural, not semantic.
requires Expressions Check Syntax, Not Correctness
A requires expression returning true means the expressions are well-formed at the point of checking — it says nothing about whether the operation is semantically meaningful or produces the right answer:
template<typename T>
concept HasSize = requires(T x) { x.size(); };
// Satisfied even if size() returns void or always throwsGuard against semantic misuse with compound requirements that constrain return types, not just expression validity.
Negation Does Not Invert Subsumption
Negating a concept in a requires clause is legal but creates an unconstrained overload that cannot be subsumed:
template<typename T>
requires (!std::integral<T>) // not part of any subsumption chain
void f(T);If you need mutually exclusive overloads, compose positive concepts explicitly rather than negating.
Compound Return Type Constraint Syntax
In { expr } -> Concept, the Concept is checked against the expression's type as if calling Concept<decltype((expr))> — the type is passed as the first argument. std::same_as<T> therefore matches the exact type including value category; std::convertible_to<T> is almost always the right choice for output constraints unless you need exact identity.
See Also
reference/language/concepts—conceptkeyword, basic definition and satisfaction rulesreference/language/concepts-advanced— partial specialization with concepts, concept maps, variable template constraintsreference/language/templates— SFINAE and pre-C++20 enable_if techniquesreference/language/ranges—std::rangesconcepts taxonomy in practice