C++ Nested Requirements in Requires Expressions

This article explains how nested requirements operate within C++20 requires expressions to enforce compile-time constraints on types and values. While standard expressions inside a requires body only check for syntactic validity, nested requirements allow developers to evaluate additional constraint expressions and concepts directly. Understanding this distinction is essential for writing robust, expressive template constraints using C++ concepts.

In C++20, a requires expression checks if a set of requirements is satisfied. There are four types of requirements: simple, type, compound, and nested. Simple requirements only verify whether an expression is well-formed and can compile. For example, writing sizeof(T) > 4; inside a requires block does not check if the size of T is actually greater than four; it only checks whether the expression sizeof(T) > 4 is syntactically valid code. Because the expression is valid for any complete type, the requirement will always pass, regardless of the evaluated result.

A nested requirement solves this problem by using an inner requires keyword inside the requires body. The syntax takes the form:

requires requires {
    requires constraint-expression;
};

When prefixed with the nested requires keyword, the compiler evaluates the constraint expression as a compile-time boolean condition. If the expression evaluates to false, the requirement is not satisfied, and template substitution fails or the concept evaluates to false.

template <typename T>
concept LargeType = requires {
    // Nested requirement: explicitly requires the condition to evaluate to true
    requires sizeof(T) > 4;
};

Nested requirements are particularly useful for constraining types that depend on other elements defined within the same requires expression. A common pattern involves inspecting a member type or the return type of an operation, and then enforcing a concept or trait on that derived type:

template <typename T>
concept Container = requires(T a) {
    typename T::value_type;
    { a.size() } -> std::same_as<std::size_t>;
    // Nested requirement enforcing a constraint on the associated type
    requires std::default_initializable<typename T::value_type>;
};

In this scenario, typename T::value_type confirms that the member type exists, while requires std::default_initializable<typename T::value_type>; asserts that the resolved type satisfies the std::default_initializable concept.

Nested requirements also allow for complex logical operations combining multiple constraints, type traits, and compile-time predicates within a single localized block:

template <typename T>
concept NumericProcessor = requires(T a) {
    requires std::is_arithmetic_v<T> && !std::is_same_v<T, bool>;
};

By explicitly evaluating boolean expressions at compile time rather than merely checking syntax, nested requirements provide the mechanism needed to enforce semantic and value-based type constraints in modern C++ templates.