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:

  1. 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
    };
  2. Type Requirements: Verify that a nested type, type alias, or template specialization exists, prefixed with typename.

    requires {
        typename T::value_type;
        typename T::iterator;
    };
  3. Compound Requirements: Enclose an expression in curly braces to check properties such as noexcept specifications and return-type constraints.

    requires(T a) {
        { a.size() } noexcept -> std::convertible_to<std::size_t>;
    };
  4. Nested Requirements: Express additional predicate constraints using another requires keyword 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 requires expression as false.
  • 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 false immediately 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.