Boolean Logic in C++ Requires Clause Conjunctions

In C++20 and later, concept conjunctions using requires clauses provide a declarative mechanism to combine compile-time predicates using the logical AND operator (&&). This article explains how the compiler normalizes these constraints into atomic constraints, executes compile-time short-circuiting to prevent invalid template instantiations, manages substitution failures, and utilizes conjunction structures for constraint subsumption during template overload resolution.

Constraint Normalization and Atomic Constraints

When the compiler evaluates a requires clause containing a conjunction, it first decomposes the expression through a process known as constraint normalization. During normalization, the compiler unfolds concepts into their underlying operations until it reaches atomic constraints. An atomic constraint is an expression that cannot be decomposed further within the constraint system—typically a constexpr bool expression or a requires expression evaluating a type's validity.

Unlike standard runtime boolean expressions, constraint conjunction operates directly on normalized constraint trees rather than raw boolean values. This allows the compiler to treat each operand as an independent logical entity rather than a monolithic boolean expression.

Compile-Time Short-Circuit Evaluation

Concept conjunction using && enforces strict compile-time short-circuit evaluation. When testing whether a concept or requires-clause is satisfied:

  1. The compiler evaluates the left-hand operand first.
  2. If the left-hand operand evaluates to false, the conjunction is immediately deemed unsatisfied.
  3. The compiler bypasses instantiation and evaluation of the right-hand operand entirely.

This behavior is critical for writing robust generic code. It allows developers to guard complex type checks that would otherwise trigger ill-formed expressions or hard compiler errors if evaluated unconditionally.

template <typename T>
concept ValidPointer = requires(T ptr) {
    *ptr;
};

template <typename T>
    requires std::is_pointer_v<T> && ValidPointer<T>
void process(T val);

In the example above, if T is an int, std::is_pointer_v<int> evaluates to false. Because of short-circuiting, the compiler never attempts to evaluate ValidPointer<int>, preventing a hard compilation error that would arise from attempting to dereference an integer inside the concept definition.

Substitution Failures vs. Constraint Non-Satisfaction

When evaluating atomic constraints within a conjunction, any substitution failure within the constraint's immediate context results in that atomic constraint evaluating to false. It does not emit a hard compile error.

If the left operand of a conjunction is satisfied, the compiler proceeds to substitute template arguments into the right operand. If substitution succeeds, the resulting expression is evaluated as a bool. If the substitution encounters an error in its immediate context, the right-hand constraint evaluates to false, causing the overall conjunction to evaluate to false.

Constraint Subsumption and Overload Resolution

Conjunctions directly influence how the compiler orders constrained templates. In C++, more constrained templates take precedence over less constrained templates via a mechanism called subsumption:

  • A constraint P && Q subsumes constraint P.
  • A constraint P && Q subsumes constraint Q.
  • P does not subsume P && Q.

During overload resolution, if two functions are viable and their constraints differ, the compiler checks if one function's constraints subsume the other. Because a conjunction explicitly demands that both sub-constraints are met, an overload constrained by A && B is strictly more specialized than an overload constrained only by A. If a call matches both overloads, the compiler selects the A && B candidate without an ambiguity error.