Document number P4281R0
Date 2026-06-20
Audience Evolution Working Group (EWG)
Reply-to Hewill Kang <hewillk@gmail.com>

Type Aliases in Requires-Expressions

Abstract

This paper proposes extending the syntax of requires-expressions to allow using-type aliases within the requirement body.

Currently, complex associated types must be spelled out repeatedly within the same constraint block. This causes code duplication and makes concepts hard to read. Allowing local type aliases inside requires-expressions eliminates redundant code without polluting the enclosing namespace.

Revision history

R0

Initial revision.

Discussion

The fundamental purpose of a typename requirement in a requires-expression is to validate that a dependent type is well-formed:


template<class T>
concept some-concept = 
  requires { typename some-type<T>; };

However, type validation rarely happens in a vacuum. In practice, once a type is proven to exist, developers almost always need to enforce further semantic constraints or concept checks on that same type:

template<class T>
concept some-concept = 
  requires { typename some-type<T>; } 
  &&    other-concept<some-type<T>>
  &&  another-concept<some-type<T>>;

When dependent type some-type<T> is a complex or deeply nested trait instantiation, spelling it out repeatedly across the constraint block creates severe text redundancy and increases the likelihood of typos.

The standard library itself provides precedents for this pattern. For example, the exposition-only concept indirectly-readable-impl ([iterator.concept.readable] ) must validate and then repeatedly constrain multiple associated types:

template<class In>
  concept indirectly-readable-impl =                    // exposition only
    requires(const In in) {
      typename iter_value_t<In>;
      typename iter_reference_t<In>;
      typename iter_rvalue_reference_t<In>;
      { *in } -> same_as<iter_reference_t<In>>;
      { ranges::iter_move(in) } -> same_as<iter_rvalue_reference_t<In>>;
    } &&
    common_reference_with<iter_reference_t<In>&&, iter_value_t<In>&> &&
    common_reference_with<iter_reference_t<In>&&, iter_rvalue_reference_t<In>&&> &&
    common_reference_with<iter_rvalue_reference_t<In>&&, const iter_value_t<In>&>;

In this single definition:

  • iter_reference_t<In> appears 4 times.
  • iter_rvalue_reference_t<In> appears 4 times.
  • iter_value_t<In> appears 3 times.
  • This repetition is highly error-prone; For user-defined concepts with similar structures but relatively complex, developers must manually re-type or copy-paste these long traits across multiple constraint positions, it is remarkably easy to accidentally omit or misplace critical cv-qualifiers or reference decorators (such as const, &, or &&). A single missed ampersand fundamentally changes the concept's semantics, leading to subtle bugs that are difficult to audit.

    To eliminate this redundancy and maximize code cohesion, we propose allowing using alias syntax directly inside the requires block, with its scope limited to that block. This allows developers to validate a type, bind it to a local name, and apply all subsequent dependent constraints within the same local scope:

    template<class In>
      concept indirectly-readable-impl =                    // exposition only
        requires(const In in) {
          using value_type       = typename iter_value_t<In>;
          using reference        = typename iter_reference_t<In>;
          using rvalue_reference = typename iter_rvalue_reference_t<In>;
          { *in } -> same_as<reference>;
          { ranges::iter_move(in) } -> same_as<rvalue_reference>;
          requires common_reference_with<reference&&, value_type&>;
          requires common_reference_with<reference&&, rvalue_reference&&>;
          requires common_reference_with<rvalue_reference&&, const value_type&>;
        };

    Noted that nesting constraints via nested requires statements means they no longer participate in concept subsumption compared to original form, this trade-off is highly beneficial for self-contained helper concepts where partial ordering is not required, since a requirement rarely stops at checking whether a type exists; it almost always serves as a baseline for further verification.

    The exact same paradigm applies to more complex, variadic components in the standard library. A prime example is the concatable concept ([range.concat.view] ) in C++26:

    template<class... Rs>
    using concat-reference-t = common_reference_t<range_reference_t<Rs>...>;      // exposition only
    template<class... Rs>
    using concat-value-t = common_type_t<range_value_t<Rs>...>;                   // exposition only
    template<class... Rs>
    using concat-rvalue-reference-t =                                             // exposition only
      common_reference_t<range_rvalue_reference_t<Rs>...>;
    
      
    template<class Ref, class RRef, class It>
      concept concat-indirectly-readable-impl =                         // exposition only
        requires (const It it) {
          { *it } -> convertible_to<Ref>;
          { ranges::iter_move(it) } -> convertible_to<RRef>;
        };
    
    template<class... Rs>
      concept concat-indirectly-readable =                              // exposition only
        common_reference_with<concat-reference-t<Rs...>&&,
                              concat-value-t<Rs...>&> &&
        common_reference_with<concat-reference-t<Rs...>&&,
                              concat-rvalue-reference-t<Rs...>&&> &&
        common_reference_with<concat-rvalue-reference-t<Rs...>&&,
                              concat-value-t<Rs...> const&> &&
        (concat-indirectly-readable-impl<concat-reference-t<Rs...>,
                                         concat-rvalue-reference-t<Rs...>,
                                         iterator_t<Rs>> && ...);
                                         
    template<class... Rs>
      concept concatable = requires {                                   // exposition only
        typename concat-reference-t<Rs...>;
        typename concat-value-t<Rs...>;
        typename concat-rvalue-reference-t<Rs...>;
      } && concat-indirectly-readable<Rs...>;
      

    By leveraging local type aliases, this massive, multi-layered infrastructure collapses cleanly into a single, highly cohesive concept definition where types and their dependent requirements are never separated or leaked into the outer scope:

    template<class... Rs>
      concept concatable = requires {                                   // exposition only
        using reference        = typename common_reference_t<range_reference_t<Rs>...>;
        using value_type       = typename common_type_t<range_value_t<Rs>...>;
        using rvalue_reference = typename common_reference_t<range_rvalue_reference_t<Rs>...>;
        requires common_reference_with<reference&&, value_type&>;
        requires common_reference_with<reference&&, rvalue_reference&&>;
        requires common_reference_with<rvalue_reference&&, value_type const&>;
        requires (requires (const iterator_t<Rs> it) {
          { *it } -> convertible_to<reference>;
          { ranges::iter_move(it) } -> convertible_to<rvalue_reference>;
        } && ...);
      };

    Under this proposal, the identifier introduced by the using alias is only visible within the enclosing requires-expression block. If the right-hand side of the using declaration fails to substitute , the requires-expression evaluates to false, matching the behavior of existing typename requirements. This is a pure syntax extension; since using tokens are currently invalid in this context, no existing code will break.

    By leveraging type-alias requirements, we can refactor the existing std::common_with concept into a single, elegant requires-expression. This not only eliminates structural duplication but also aligns the implementation perfectly with its underlying mathematical and narrative constraints:

    Status Quo With this proposal
    template<class T, class U>
    concept common_with =
      same_as<common_type_t<T, U>, common_type_t<U, T>> &&
      requires {
        static_cast<common_type_t<T, U>>(declval<T>());
        static_cast<common_type_t<T, U>>(declval<U>());
      } &&
      common_reference_with<
        add_lvalue_reference_t<const T>,
        add_lvalue_reference_t<const U>> &&
      common_reference_with<
        add_lvalue_reference_t<common_type_t<T, U>>,
        common_reference_t<
          add_lvalue_reference_t<const T>,
          add_lvalue_reference_t<const U>>>;
    
    template<class T, class U>
    concept common_with = requires {
      using CommonTU = typename common_type_t<T, U>;
      using CommonUT = typename common_type_t<U, T>;
      requires same_as<CommonTU, CommonUT>;
    
      static_cast<CommonTU>(declval<T>());
      static_cast<CommonTU>(declval<U>());
    
      using RefT = typename add_lvalue_reference_t<const T>;
      using RefU = typename add_lvalue_reference_t<const U>;
      requires common_reference_with<RefT, RefU>;
    
      using RefCommon = typename add_lvalue_reference_t<CommonTU>;
      using CommonRef = typename common_reference_t<RefT, RefU>;
      requires common_reference_with<RefCommon, CommonRef>;
    };
    

    The same architectural simplification also applies to type traits implementation, as this feature is exceptionally useful for defining intricate type traits, particularly those specified with patterns like: "Let U be some type. If U is well-formed and certain additional constraints are met, then the type trait denotes U." The standard's definition of common_reference (([meta.trans.other] )) is precisely such a trait, governed by a highly complex set of conditional bullets:

    -6- For the common_reference trait applied to a parameter pack T of types, the member type shall be either declared or not present as follows:

    1. […]
    2. (6.3) — Otherwise, if sizeof...(T) is two, let T1 and T2 denote the two types in the pack T. Then:

      1. (6.3.1) — Let R be COMMON-REF(T1, T2). If T1 and T2 are reference types, R is well-formed, and is_convertible_v<add_pointer_t<T1>, add_pointer_t<R>> && is_convertible_v<add_pointer_t<T2>, add_pointer_t<R>> is true, then the member typedef type denotes R.

    With the introduction of this feature, we no longer need to translate these narrative rules into heavy, unmaintainable template metaprogramming boilerplate. Instead, we can implement the entire core deduction logic directly and intuitively using constexpr if statements with block-scoped aliases:

    template<class T1, class T2>
    consteval auto compute-common-reference() {
        // ...
        // (6.3.1) COMMON-REF check for reference types
        if constexpr (requires {
          using R = typename COMMON-REF(T1, T2); 
          requires is_reference_v<T1>;
          requires is_reference_v<T2>;
          requires is_convertible_v<add_pointer_t<T1>, add_pointer_t<R>>;
          requires is_convertible_v<add_pointer_t<T2>, add_pointer_t<R>>;
        })
          return type_identity<typename COMMON-REF(T1, T2)>{};
        // ...
    }

    As demonstrated above, this feature dramatically unifies the developer experience across both language layers. Whether an implementer is writing a consteval type-deduction engine or defining a foundational core concept, block-scoped aliases eliminate external boilerplate, safeguard substitution errors through clean short-circuiting, and turn impenetrable metaprogramming into a readable, top-down sequence.

    Moreover, The motivation for this feature is reinforced by the introduction of reflection. In C++26, the type-requirement production rule already supports the splice-specifier syntax, allowing developers to directly validate types generated from meta-objects via typename [: ... :];. However, when developers need to perform multiple consecutive constraint checks on the same spliced type, they are currently forced to duplicate the entire dynamic expression repeatedly across the requirement body, introducing substantial structural redundancy.

    With this feature, we can significantly simplify such checks by binding the spliced type to a local name at the first introduction. This allows all subsequent constraints to be applied directly to a clean identifier, eliminating the need to re-evaluate or re-spell the complex reflection syntax:

    Status Quo With this proposal
    Example 1: Verifying and constraining a reflected template specialization
    template<typename T> concept C = requires {
      // T::r2 is a reflection of a template Z for which Z<int> is a type
      typename [:T::r2:]<int>;
      requires std::ranges::range<typename [:T::r2:]<int>>;
      requires std::same_as<typename [:T::r2:]<int>::value_type, int>;
    };
    template<typename T> concept C = requires {
      // T::r2 is a reflection of a template Z for which Z<int> is a type
      using Container = typename [:T::r2:]<int>;
      requires std::ranges::range<Container>;
      requires std::same_as<Container::value_type, int>;
    };
    Example 2: Validating and constraining a reflected return type
    template<typename T> concept C = requires {
      typename T::method;
      typename [: std::meta::return_type_of(^^T::method) :];
    } && 
      std::floating_point<typename [: std::meta::return_type_of(^^T::method) :]>;
    template<typename T> concept C = requires {
      using MethodType = typename T::method;
      using RetType = typename [: std::meta::return_type_of(^^MethodType) :];
      requires std::floating_point<RetType>;
    };
    Example 3: Checking traits on a reflected enum underlying type
    template<typename T> concept C = requires {
      // T::enum_type is a reflection of a enum type
      T::enum_type;
      typename [: std::meta::underlying_type(T::enum_type) :];
    } && 
      sizeof(typename [: std::meta::underlying_type(T::enum_type) :]) >= 4;
    
    template<typename T> concept C = requires {
      // T::enum_type is a reflection of a enum type
      T::enum_type;
      using IntType = typename [: std::meta::underlying_type(T::enum_type) :];
      requires (sizeof(IntType) >= 4);
    };

    In summary, allowing using declarations inside the requirement body provides an elegant, unified solution. It ensures that dependent type verification and subsequent semantic constraints remain strictly localized and heavily cohesive.

    Design

    typename is Optional in Local Aliases

    A key design question for this feature is whether the typename keyword should be required or optional on the right-hand side of a local type alias. We propose the latter.

    Forcing users to write the typename keyword is unnecessary and introduces redundant boilerplate. In ordinary code outside of constraints, developers can already omit typename in contexts that are unambiguously known to be types.

    A local alias inside a requires-expression behaves exactly like a standard alias declaration in this regard, as the parser already understands that the right-hand side of a using statement must resolve to a type. Forcing developers to write typename would create an inconsistent user experience for no tangible benefit. Therefore, omitting typename must be allowed for these common cases.

    Non-support for Local Alias Templates

    We explicitly design this feature to support only local type aliases, excluding local alias templates. Arthur O'Dwyer provided a hypothetical use case where an allocator's rebind structure is bound locally inside a constraint:

    template<class A>
      concept Allocator = requires (const A& a) {
        template<class T> using rebound = typename std::allocator_traits<A>::template rebind_alloc<T>;
        rebound<int>(a);
        requires std::same_as<typename rebound<int>::value_type, int>;
      };

    We believe introducing local template templates inside requires-expressions adds unnecessary complexity to the parser for little practical gain. If a generic template alias is truly needed, it should be declared externally. If it is only needed for specific validation within the concept, developers can simply bind the concrete instantiated type locally:

    template<class A>
      concept Allocator = requires (const A& a) {
        using rebound_int = typename std::allocator_traits<A>::template rebind_alloc<int>;
        rebound_int(a);
        requires std::same_as<typename rebound_int::value_type, int>;
      };

    The primary goal of this proposal is to eliminate text redundancy for deeply nested associated types via simpler typename bindings. Restricting the feature to non-template aliases keeps the implementation footprint minimal for compiler front-ends while still solving the vast majority of real-world pain points.

    Proposed change

    This wording is relative to Lastest Working Draft.

  • Modify 7.5.8.3 [expr.prim.req.type], Type requirements, as indicated:

    type-requirement:

    1. typename required-type-idnested-name-specifieropt type-name;
    2. typename splice-specifier;
    3. typename splice-specialization-specifier;
    4. type-alias-requirement

    type-alias-requirement:

    1. using identifier = typenameopt required-type-id ;

    required-type-id:

    1. nested-name-specifieropt type-name
    2. splice-specifier
    3. splice-specialization-specifier

    -1- A type-requirement asserts the validity of a type. A type-alias-requirement introduces a new alias name into the enclosing block scope that denotes the valid type. The component names of a type-requirement that is not a type-alias-requirement are those of its required-type-idits nested-name-specifier (if any) and type-name (if any).

    [Note 1: The enclosing requires-expression will evaluate to false if substitution of template arguments fails, which, for a type-alias-requirement, includes the substitution into the type denoted by the alias. — end note]

    [Example 1:

    template<typename T, typename T::type = 0> struct S;
    template<typename T> using Ref = T&;
    
    template<typename T> concept C = requires {
      typename T::inner;        // required nested member name
      typename S<T>;            // required valid template-id; fails if T::type does not exist as a type
      typename Ref<T>;          // required alias template substitution, fails if T is void
      typename [:T::r1:];       // fails if T::r1 is not a reflection of a type
      typename [:T::r2:]<int>;  // fails if T::r2 is not a reflection of a template Z for which Z<int> is a type
    
      using U = T::inner;              // OK: U denotes T::inner if valid; typename omitted
      using V = typename T::inner;     // OK: explicit typename is also allowed
      requires std::same_as<U, int>;   // OK: U is in scope
      template<class X> using W = X&;  // error: template declaration is not allowed within a type-requirement
    
      using R1 = [:T::r1:];            // OK: R1 denotes the spliced type if valid
      requires requires {
        requires sizeof(R1) == 1;      // OK: R1 is in scope
      };
    };

    end example]

    -2- A type-requirement that names a class template specialization does not require that type to be complete ([basic.types.general]).