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.
requires-expression inside requires-clause
A requires expression can appear directly inside a requires clause without naming a concept. This is the requires requires double form — the outer requires is the clause gating the template, and the inner requires begins the expression being evaluated.
// C++20
// Inline constraint: T must support .push_back() taking a value_type
template<typename T>
requires requires(T& c, typename T::value_type v) {
c.push_back(v);
}
void append_one(T& container, typename T::value_type val) {
container.push_back(val);
}
// When the same expression recurs, extract it into a named concept instead
template<typename T>
concept PushBackable = requires(T& c, typename T::value_type v) {
c.push_back(v);
};
template<PushBackable T>
void append_one(T& container, typename T::value_type val) { // C++20
container.push_back(val);
}The inline form is useful for one-off constraints at a single callsite, but named concepts should be preferred the moment the same predicate is needed in more than one template — only named concepts participate in subsumption.
Compound requirements
Compound requirements check both that an expression is valid and that its return type satisfies a concept. The syntax { expr } -> ConceptName desugars to: first evaluate expr without triggering a hard error; then check ConceptName<decltype((expr))>.
// C++20
template<typename T>
concept Sizeable = requires(const T& c) {
// expression must be valid AND return type must be convertible to size_t
{ c.size() } -> std::convertible_to<std::size_t>;
};
template<typename T>
concept CloneableNoexcept = requires(const T& x) {
// noexcept specifier makes the constraint also require a non-throwing call
{ x.clone() } noexcept -> std::same_as<T>; // C++20
};
template<typename T>
concept Iterator = requires(T it) {
{ *it } -> std::same_as<typename T::reference>;
{ ++it } -> std::same_as<T&>;
{ it++ } -> std::same_as<T>;
};Prefer std::convertible_to<R> over std::same_as<R> for return-type constraints unless you need exact type identity — convertible_to passes through implicit conversions and is almost always the correct choice for library interfaces.
Nested requirements
A nested requirement embeds an arbitrary constant expression as an additional constraint inside a requires expression. Use the requires keyword again as the third "requires" form: requires ConstantExpression; inside the body of a requires expression.
// C++20
template<typename T>
concept SmallTriviallyCopyable = requires {
requires std::is_trivially_copyable_v<T>; // nested: trait must hold
requires (sizeof(T) <= 16); // nested: size limit
};
template<typename T>
concept SignedArithmetic = requires(T a, T b) {
a + b;
a - b;
a * b;
requires std::signed_integral<T> || std::floating_point<T>; // nested
};
// Practical use: constrain on the range's value type inside one concept
template<typename R>
concept FloatRange =
std::ranges::input_range<R> &&
requires {
requires std::floating_point<std::ranges::range_value_t<R>>; // C++20
};Nested requirements are evaluated as ordinary constant expressions; if one is ill-formed as a constant expression the whole requires expression produces false, not a hard error.
Concept subsumption and partial ordering
Subsumption allows the compiler to rank overloads by constraint strength without ambiguity. Concept A subsumes concept B when every atomic constraint in B's normalized form also appears in A's normalized form under logical conjunction — meaning A's constraint set is provably a superset of B's.
// C++20
template<typename T>
concept Hashable = requires(T x) {
{ std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};
template<typename T>
concept HashableAndOrdered = Hashable<T> && std::totally_ordered<T>;
// ^^^ subsumes Hashable because Hashable<T> is a conjunct in its definition
template<Hashable T>
std::size_t bucket(T key); // less constrained
template<HashableAndOrdered T>
std::size_t bucket(T key); // more constrained — selected when both are satisfied
bucket(42); // int satisfies both; HashableAndOrdered overload wins
bucket(std::string{"x"}); // same — more constrained overload winsSubsumption is structural, not semantic. Two concepts with identical bodies but different names do not subsume each other; only explicit syntactic composition via && or || establishes a subsumption relationship.
// C++20 — subsumption failure due to aliasing without composition
template<typename T>
concept EvenInt = std::integral<T>; // copy of body, not composition
template<typename T>
concept OddInt = std::integral<T>; // same body
template<EvenInt T> void f(T);
template<OddInt T> void f(T); // ambiguous — neither subsumes the otherAbbreviated function templates
The auto placeholder in a function parameter list creates an implicit template parameter, and pairing it with a concept name produces a concise constrained function template. This abbreviated syntax is equivalent to a full explicit template declaration with a constrained parameter.
// C++20 — all three declarations are equivalent
template<std::integral T>
T increment(T x) { return x + 1; }
template<typename T>
requires std::integral<T>
T increment(T x) { return x + 1; }
std::integral auto increment(std::integral auto x) { return x + 1; }
// ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
// return type constrained parameter constrained
// Multiple auto parameters — each gets its own independent implicit T
void swap_print(std::totally_ordered auto a, std::totally_ordered auto b) {
if (a > b) std::swap(a, b);
std::cout << a << ' ' << b << '\n';
}
// Note: a and b may be DIFFERENT types — use explicit template<T> if they must matchThe abbreviated form shines for single-parameter utilities and lambdas; for multi-parameter templates where type relationships matter, the explicit template<typename T> form avoids surprising independent deduction.
// C++20 — constrained lambda using abbreviated syntax
auto square = [](std::arithmetic auto x) { return x * x; };
// std::arithmetic is not in the standard — use a user-defined concept or:
auto square2 = [](auto x) requires (std::integral<decltype(x)> ||
std::floating_point<decltype(x)>) {
return x * x;
};Constraining class templates
A requires clause after the template<> head constrains every instantiation of a class template. Member functions and member function templates can carry their own additional requires clauses that narrow their availability further.
// C++20
template<typename T>
requires std::is_trivially_destructible_v<T>
class FixedBuffer {
T data_[128];
public:
// Member available only when T is also copy-constructible
void fill(const T& val) requires std::copy_constructible<T>;
// Member available only when T is move-constructible
void fill(T&& val) requires std::move_constructible<T>;
// Non-type template member with its own constraint
template<std::size_t N>
requires (N <= 128)
void copy_to(T (&arr)[N]) const;
};
// Specializations may relax or tighten constraints independently
template<std::integral T>
class FixedBuffer<T> {
// Integer-specific specialization — different set of methods
};Class template constraints are checked at the point of instantiation, not at the point of declaration. An unconstrained member function template inside a constrained class is only reachable when the class itself is already instantiated, so the class constraint is always evaluated first.
Concepts in standard library (std::ranges)
The <concepts> and <ranges> headers ship a rich taxonomy of pre-defined concepts that cover the vast majority of generic programming needs. Prefer these over hand-rolled equivalents whenever possible; they are carefully specified and participate in subsumption chains across the entire standard library.
// C++20 — core language concepts from <concepts>
static_assert(std::same_as<int, int>);
static_assert(std::derived_from<std::vector<int>, std::vector<int>>);
static_assert(std::convertible_to<int, double>);
static_assert(std::integral<long>);
static_assert(std::floating_point<float>);
static_assert(std::copyable<std::string>);
static_assert(std::movable<std::unique_ptr<int>>);
static_assert(std::regular<int>); // copyable + equality_comparable
static_assert(std::semiregular<int>); // copyable + default_initializable// C++20 — range concepts from <ranges>
#include <ranges>
#include <vector>
#include <list>
#include <forward_list>
static_assert(std::ranges::random_access_range<std::vector<int>>);
static_assert(std::ranges::bidirectional_range<std::list<int>>);
static_assert(std::ranges::forward_range<std::forward_list<int>>);
static_assert(std::ranges::contiguous_range<std::vector<int>>);
static_assert(std::ranges::sized_range<std::vector<int>>);
static_assert(!std::ranges::sized_range<std::forward_list<int>>);
// range_value_t, range_reference_t, range_difference_t work with any range
using V = std::ranges::range_value_t<std::vector<double>>; // double // C++20// C++20 — algorithm constraint example using std::ranges concepts
template<std::ranges::input_range R,
std::indirect_unary_predicate<std::ranges::iterator_t<R>> Pred>
std::ranges::range_difference_t<R> count_if(R&& range, Pred pred) {
std::ranges::range_difference_t<R> n = 0;
for (auto&& elem : range)
if (pred(elem)) ++n;
return n;
}The std::ranges concepts form a strict hierarchy — random_access_range refines bidirectional_range, which refines forward_range, and so on — so subsumption automatically selects the most specialized algorithm overload for a given range type.
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