Skip to content
C++
Language
since C++20
Intermediate

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++20

A 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

cpp
// 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

cpp
// 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++20

Applying Concepts — Four Equivalent Syntaxes

cpp
// 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

cpp
#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 Arithmetic

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

cpp
// 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 overload

Subsumption 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

cpp
// 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:

cpp
// 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:

cpp
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

cpp
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 other

The 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:

cpp
template<typename T>
concept HasSize = requires(T x) { x.size(); };
// Satisfied even if size() returns void or always throws

Guard 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:

cpp
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/conceptsconcept keyword, basic definition and satisfaction rules
  • reference/language/concepts-advanced — partial specialization with concepts, concept maps, variable template constraints
  • reference/language/templates — SFINAE and pre-C++20 enable_if techniques
  • reference/language/rangesstd::ranges concepts taxonomy in practice