C++ Concepts: Compound Requirements and Return Types
In modern C++, concepts allow developers to define semantic and
syntactic constraints on template parameters. Within a
requires expression, compound requirements verify the
validity of an expression while optionally enforcing
noexcept guarantees and evaluating the resulting return
type against a type constraint. This article explores how compound
requirement checking processes and validates these return types,
focusing on expression value categories,
decltype((expression)) substitution, and placeholder type
deduction mechanics.
Anatomy of a Compound Requirement
A compound requirement in a C++ requires expression uses
the following syntax:
{ expression } noexcept(optional) -> type-constraint;When the compiler evaluates this construct, it executes the following verification pipeline in order:
- Validity: Evaluates whether
expressionis valid and well-formed. - Exception Specification (Optional): If
noexceptis specified, verifies thatexpressionis declared not to throw exceptions. - Return Type Checking (Optional): If a trailing
-> type-constraintis present, deduces the type of the expression and verifies that it satisfies the constraint.
If any of these stages fail, the entire compound requirement
evaluates to false, causing the enclosing concept to not be
satisfied.
The Return Type Evaluation Mechanism
When evaluating the trailing type-constraint, the
compiler does not simply extract the plain value type. Instead, it
follows precise rules governed by template argument deduction for
constrained placeholders.
1. Deducing
decltype((expression))
The compiler evaluates the return type as
decltype((expression)). The double parentheses are
critical: they ensure that the expression's value category is preserved
in the resulting type:
- Lvalue: Evaluates to an lvalue reference
(
T&). - Xvalue: Evaluates to an rvalue reference
(
T&&). - Prvalue: Evaluates to a non-reference type
(
T).
Because reference qualifiers are preserved, return type constraints must account for whether a function returns by value, reference, or rvalue reference.
2. Placeholder Type Deduction
The return-type requirement -> Concept behaves
conceptually as if the deduced type is assigned to a constrained
auto variable:
Concept auto __deduced = expression; // conceptually, using decltype(auto) semanticsMore specifically, the language rules define that
decltype((expression)) must satisfy the type
constraint:
- If the constraint is a bare concept name
C, the compiler checksC<decltype((expression))>. - If the constraint includes additional arguments
C<Args...>, the deduced type is supplied as the first template argument:C<decltype((expression)), Args...>.
Common Constraints and Value Categories
Because the evaluated type includes reference qualifiers, different standard concepts behave distinctly when used in compound requirements.
Exact Type Matching with
std::same_as
The standard concept std::same_as<T, U> checks
that both types are identical. Because decltype((expr))
preserves references, exact matching can produce unexpected results if
references are present:
template <typename T>
concept ReturnsExactInt = requires(T t) {
{ t.getValue() } -> std::same_as<int>;
};- If
t.getValue()returnsint, the deduced type isint(prvalue). The checkstd::same_as<int, int>evaluates totrue. - If
t.getValue()returnsint&, the deduced type isint&(lvalue). The checkstd::same_as<int&, int>evaluates tofalse.
To accept both values and references, the requirement must either
specify the reference explicitly
(std::same_as<int&>) or check the
decayed/underlying type using a custom constraint.
Implicit Conversion
with std::convertible_to
A common idiom is requiring that an expression's result can be converted to a specific target type:
template <typename T>
concept Stringifiable = requires(T t) {
{ t.toString() } -> std::convertible_to<std::string>;
};This checks
std::convertible_to<decltype((t.toString())), std::string>.
This verifies that decltype((t.toString())) can be
implicitly and explicitly converted to std::string,
seamlessly accommodating lvalue references, rvalue references, and value
types such as std::string_view or
const char*.
Summary of the Evaluation Steps
When the compiler resolves
{ expr } -> Concept<Args...>;:
- Check if
expris syntactically valid in an unevaluated context. - Determine
U = decltype((expr)). - Construct the constraint check
Concept<U, Args...>. - Normalize and evaluate the associated concept constraints with
Uas the first argument. - If constraint satisfaction yields
true, the return type requirement passes.