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.