Skip to content
C++

Writing requires Expressions

A requires expression is the building block of C++20 constraints. It evaluates to a compile-time boolean: true when a set of requirements on template arguments are all satisfied, false otherwise. Understanding the four distinct kinds of requirement — simple, type, compound, and nested — lets you express precisely what a type must support without resorting to SFINAE or type trait gymnastics.

The structure of a requires expression

A requires expression resembles a function signature followed by a body. The optional parameter list introduces local names whose only purpose is to appear in the requirement sequence — no storage is allocated, no function is ever called. The parameter list can be omitted when no local name is needed (the expression closes over the template parameters directly). Each requirement inside the braces must end with a semicolon.

requires (parameter-list-opt) { requirement-seq }

// Examples of the two syntactic forms:
template<typename T>
concept Addable = requires(T a, T b) {  // local params a, b
    a + b;                               // requirement using them
};

template<typename T>
concept Arithmetic = requires {          // no local params needed
    std::is_arithmetic_v<T>;            // uses T from outer scope
};

A requires expression is not the same as a requires clause. The clause — the requires keyword followed by a compile-time boolean expression after a template parameter list or after a function declaration — controls whether a template participates in overload resolution. A requires expression is just one kind of boolean expression that can appear in a requires clause. The distinction matters because a requires expression can also appear on the right-hand side of a concept definition, or anywhere a compile-time boolean is legal.

// requires clause: selects which overload is viable
template<typename T>
requires std::is_arithmetic_v<T>      // [requires clause — uses a type trait directly]
T add(T a, T b) { return a + b; }

// requires clause using a requires expression:
template<typename T>
requires requires(T a, T b) { a + b; }   // [requires clause] [requires expression]
T add(T a, T b) { return a + b; }        // the "double requires" is intentional

Simple requirements: checking expression validity

A simple requirement is an arbitrary expression — not a boolean, just any expression. The compiler checks that the expression is syntactically and semantically valid when template parameters are substituted in. If the expression is ill-formed (would not compile), the requirement evaluates to false and the constraint is not satisfied. The expression itself is never actually evaluated at runtime; it is purely checked for correctness. The expression must not begin with the requires keyword, which would make it a nested requirement instead.

template<typename T>
concept Addable = requires(T a, T b) {
    a + b;          // simple: checks that operator+ is defined for T
};

template<typename T>
concept Incrementable = requires(T t) {
    ++t;            // simple: checks prefix increment exists
    t++;            // simple: checks postfix increment exists
};

// A "logger" interface: T must have error/warning/info methods
// taking a const char* argument
template<typename T>
concept Logger = requires(T t) {
    t.error("msg");    // simple: method must exist and accept a string literal
    t.warning("msg");
    t.info("msg");
};

// Use: any type with these methods satisfies Logger<T>
template<Logger L>
void report_problem(L& log, std::string_view msg) {
    log.error(msg.data());
}

Type requirements: checking that a type exists

A type requirement is introduced by the keyword typename followed by a type name. It checks that the named type is valid — that a nested type member exists, that a class template specialization is well-formed, or that a template alias specialization names a type. If the type does not exist, the requirement is false. This replaces the pattern of using std::void_t and SFINAE to check for nested types.

// Check that T has key_type and value_type nested types
template<typename T>
concept KeyValuePair = requires {
    typename T::key_type;       // type requirement: nested type must exist
    typename T::value_type;
};

struct MyPair {
    using key_type   = int;
    using value_type = std::string;
};

static_assert(KeyValuePair<MyPair>);             // satisfies — both types exist
static_assert(!KeyValuePair<std::pair<int,int>>); // fails — wrong names (first/second_type)

// Check that a class template specialization is valid
template<typename T>
requires std::is_arithmetic_v<T>
struct numeric_wrapper { T value; };

template<typename T>
concept Wrappable = requires {
    typename numeric_wrapper<T>;  // type requirement: specialization must be valid
};

static_assert(Wrappable<int>);     // numeric_wrapper<int> is valid
static_assert(!Wrappable<std::string>); // numeric_wrapper<std::string> fails

Compound requirements: checking expression properties

A compound requirement goes further than a simple requirement. It checks not only that an expression is valid, but optionally also that it does not throw exceptions and that its result type satisfies a type constraint. The syntax encloses the expression in braces, followed by optional noexcept and an optional return type constraint introduced by ->.

{ expression } noexcept(opt) -> type-constraint(opt) ;

The type constraint after -> is not just any type — it must be a concept, not a raw type or a type trait. The compiler applies the constraint to decltype((expression)), checking that the return type satisfies the named concept. This is how you require "this method returns something convertible to X" or "this returns the same type as Y."

#include <concepts>

// Timer concept: start() → void (noexcept), stop() → convertible to long long
template<typename T>
concept Timer = requires(T t) {
    { t.start() } noexcept -> std::same_as<void>;
    { t.stop()  }           -> std::convertible_to<long long>;
};

struct HighResTimer {
    void start() noexcept {}
    long long stop() { return 0; }
};

struct BadTimer {
    void start() {}         // NOT noexcept — fails the noexcept requirement
    void stop() {}          // return type void, not convertible to long long
};

static_assert( Timer<HighResTimer>);
static_assert(!Timer<BadTimer>);

// NonThrowing: callable F with args T... must not throw
template<typename F, typename... T>
concept NonThrowing = requires(F&& func, T... t) {
    { func(t...) } noexcept;   // expression must be noexcept
};

// Only accepts functions marked noexcept
template<typename F, typename... T>
requires NonThrowing<F, T...>
void safe_invoke(F&& f, T... args) { f(args...); }

Nested requirements: embedding constraints inside requires

A nested requirement embeds a constraint expression inside a requires expression. It is introduced by the requires keyword (which distinguishes it from a simple requirement, which cannot begin with that keyword). Its purpose is to impose additional conditions that cannot be expressed as simple expression validity checks — for example, a compile-time boolean predicate on a property of the pack, or a size check.

requires constraint-expression ;    // nested requirement syntax
#include <concepts>

// HomogenousRange: variadic args must all have the same type AND there must be ≥2
template<typename T, typename... Ts>
inline constexpr bool are_same_v =
    std::conjunction_v<std::is_same<T, Ts>...>;

template<typename... T>
concept HomogenousRange = requires(T... t) {
    (... + t);                    // simple: fold + is valid for all T
    requires are_same_v<T...>;    // nested: all types must be the same
    requires sizeof...(T) > 1;   // nested: at least two arguments
};

template<typename... T>
requires HomogenousRange<T...>
auto sum(T&&... t) { return (... + t); }

sum(1, 2, 3);         // OK: all int, ≥2 args
// sum(1, 2.0);       // error: types differ
// sum(42);           // error: fewer than 2 args

All four kinds in one concept

Real-world concepts combine several requirement kinds. Here is a concept for a container type that illustrates all four simultaneously. The container concept shows how you would formally characterize what it means to be a standard-library-style container, replacing the unwieldy std::void_t SFINAE that pre-C++20 code required for the same check.

template<typename T>
concept Container = requires(T c) {
    // Type requirements: nested type members must exist
    typename T::value_type;
    typename T::size_type;
    typename T::iterator;
    typename T::const_iterator;

    // Simple requirements: methods must be callable
    c.size();
    c.begin();
    c.end();

    // Compound requirements: begin() must return an iterator
    { c.begin() } -> std::same_as<typename T::iterator>;
    { c.end()   } -> std::same_as<typename T::iterator>;

    // Nested requirement: size_type must be unsigned
    requires std::unsigned_integral<typename T::size_type>;
};

static_assert(Container<std::vector<int>>);
static_assert(Container<std::list<std::string>>);
// static_assert(Container<int>);   // fails — int has no value_type, begin(), etc.

template<Container C>
void print_all(const C& c) {
    for (const auto& v : c)
        std::cout << v << ' ';
}

Quick reference: requirement kinds

Simple requirement

expression ;

Expression is syntactically/semantically valid (not evaluated at runtime).

a + b; t.push_back(v);

Type requirement

typename type-name ;

The named type exists (nested type, template specialization, or alias).

typename T::value_type; typename Vec<T>;

Compound requirement

{ expr } noexcept? -> concept? ;

Expression valid; optionally: does not throw; return type satisfies concept.

{ t.size() } noexcept -> std::convertible_to<size_t>;

Nested requirement

requires constraint-expression ;

An arbitrary compile-time boolean constraint is satisfied.

requires std::is_signed_v<T>; requires sizeof...(Ts) > 0;
Sign in to track progress