C++ Requires Expressions: Compile-Time Validation
This article provides an overview of the C++20 requires
expression, examining its role in compile-time template programming. It
covers the fundamental syntax of requires expressions,
explores the four types of requirements used to test code validity, and
explains how the compiler checks expressions to yield boolean values
rather than hard compilation errors.
A requires expression is an unevaluated, compile-time
predicate introduced in C++20 that evaluates to true if a
given set of syntactic and semantic requirements is valid for specified
types, and false otherwise. It serves as the primary engine
behind C++ Concepts, allowing developers to inspect whether an arbitrary
expression, type alias, or member function call is well-formed before
compiling code that depends on it.
Anatomy of a Requires Expression
A requires expression uses the following general
structure:
requires (parameter-list) {
requirement-seq
}The parameter list is optional and introduces dummy variables used solely for testing expressions without constructing real objects at runtime. The requirement sequence contains the constraints that must be verified.
Unlike a requires clause (which conditionally constrains
a template), a requires expression is a primary expression
that yields a bool. It can be used directly in concepts,
template constraints, or if constexpr blocks:
template <typename T>
concept Printable = requires(T x) {
std::cout << x;
};The Four Requirement Types
The compiler validates syntax validity through four specific forms within the requirement body:
Simple Requirements: Assert that an arbitrary expression is valid.
requires(T a, T b) { a + b; // Valid if operator+ exists for type T a.clear(); // Valid if member function clear() exists };Type Requirements: Verify that a nested type, type alias, or template specialization exists, prefixed with
typename.requires { typename T::value_type; typename T::iterator; };Compound Requirements: Enclose an expression in curly braces to check properties such as
noexceptspecifications and return-type constraints.requires(T a) { { a.size() } noexcept -> std::convertible_to<std::size_t>; };Nested Requirements: Express additional predicate constraints using another
requireskeyword inside the body, useful for checking compile-time constants.requires(T a) { requires sizeof(a) >= 4; requires std::is_integral_v<T>; };
How Syntax Validity Is Validated at Compile Time
During compilation, the compiler attempts to substitute the provided
template arguments into the declarations and expressions specified
within the requires block:
- Substitution and SFINAE: The compiler processes the
operations sequentially. If substitution fails at any point—such as an
invalid operator lookup, an inaccessible member, or a missing type
alias—the compiler suppresses the error and treats the entire
requiresexpression asfalse. - Unevaluated Operands: Expressions inside the body are unevaluated. No code is generated, no functions are called, and objects are not constructed in memory. The compiler only verifies type-checking, access permissions, and overload resolution.
- Short-Circuiting Within Sequences: If any
individual requirement within the sequence is invalid, substitution
stops or marks the expression invalid, returning
falseimmediately for that constraint check.
By translating potential compile-time failures into boolean values,
requires expressions replace complex SFINAE techniques
using std::enable_if with declarative, readable syntax.