Extending constant template parameter support
via operator template()
Theme
Contents

Extending constant template parameter support
via operator template()

Extending constant template parameter support
via operator template()

Document #: P4340R1 Latest Status
Date: 2026-11-10
Project: Programming Language C++
Audience: EWG
Reply-to: Barry Revzin
<>

1 Introduction

This is a follow-up to [P2484R0] (Extending class types as non-type template parameters) and [P3380R1] (Extending support for class types as non-type template parameters) (the latter of which starts with a useful reading list), and is a new solution to that problem building upon three insights:

The goal here is to allow types to opt-in to being usable as constant template parameters in a way that’s forward-looking to containers as well. This proposal will also make the following standard library types usable: std::optional, std::expected, std::variant, std::tuple, std::string_view, std::span, most types in <chrono>, std::complex, the comparison categories, std::reference_wrapper, std::inplace_vector, and std::bitset. Additionally, string literals will also become usable.

2 Design

One of the many consteval functions added to the standard library by [P2996R13] (Reflection for C++26) was std::meta::reflect_constant(v). This is a very interesting function in that, if v has class type, this returns a reflection representing the template parameter object that is template-argument-equivalent to v.

That is, for all class types that are today usable as constant template parameters, this comparison holds (let’s agree to ignore that I’m not using std::addressof):

template <auto V>
constexpr void const* addr = &V;

static_assert(addr<E> == &[: std::meta::reflect_constant(E) :]);

Ultimately, the whole problem of how to opt an arbitrary type with non-public subobjects into being usable as a constant template parameter is about this question of how to produce the template parameter object. What if we cut out the middle-man and simply allowed (forced) types to directly produce that template parameter object?

Here is a very simple example:

class Widget {
    // private, so not usable by default
    int data_;

public:
    constexpr Widget(int d) : data_(d) { }

    consteval operator template() const;
};

template <int D> constexpr auto impl = Widget(D);

consteval Widget::operator template() const {
    return substitute(^^impl, {std::meta::reflect_constant(data_)});
}

Widget is defining how to produce a template parameter object: operator template returns a reflection representing a static storage duration object. It is up to Widget to figure out how to do that, but once it does so, we have already solved all the other problems in this space:

std::meta::reflect_constant(v) will then be extended to first invoke v.operator template() for class types that opt in before falling back to the normal C++20 structural rules. This function is otherwise not utterable by the user. More on this in the naming section.

2.1 Normalizing

Let’s go through two examples where we do something non-trivial: we want to do some normalization on the way in, so that we have fewer or more predictable specializations. Previous papers in this space brought up two examples:

Let’s start with Fraction. We need to make sure that we serialize it in lowest terms, so the arguments we’re substitute()-ing will have to be reduced:

struct Frac {
    int numer;
    int denom;
    consteval operator template() const;
};
template <int N, int D> constexpr auto interned = Frac{N, D};

consteval Frac::operator template() const {
    int g = std::gcd(numer, denom);
    return substitute(^^interned, {
        std::meta::reflect_constant(numer / g),
        std::meta::reflect_constant(denom / g),
    });
}

And the result we will end up with is:

template <auto V> inline constexpr auto const& ref = V;

// the template parameter object is {1, 2}, that's all we see
static_assert(ref<Frac{2, 4}>.numer == 1);
static_assert(ref<Frac{2, 4}>.denom == 2);

// there is only one template parameter object, so these
// alternate representations have the same address
static_assert(&ref<Frac{2, 4}> == &ref<Frac{3, 6}>);

Similarly, the SmallString container I showed in [P3380R1] wanted to ensure that regardless of what path you get to the same “value” that you only see the real value, and not any trailing characters. We can do that too:

class SmallString {
    char data_[32];
    int length_ = 0; // always <= 32

public:
    SmallString() = default;

    constexpr SmallString(std::initializer_list<char> elems)
        : data_(), length_(elems.size())
    {
        std::copy(elems.begin(), elems.end(), data_);
    }

    constexpr auto data() const -> char const* {
        return data_;
    }

    constexpr auto push_back(char c) -> void {
        assert(length_ < 32);
        data_[length_] = c;
        ++length_;
    }

    constexpr auto pop_back() -> void {
        assert(length_ > 0);
        --length_;
    }

    constexpr auto operator[](size_t idx) const -> char {
      return data_[idx];
    }

    consteval operator template() const;
};

template <char... Cs>
inline constexpr SmallString impl = {Cs...};

consteval SmallString::operator template() const {
    std::vector<std::meta::info> elems;
    for (size_t i = 0; i != length_; ++i) {
        elems.push_back(std::meta::reflect_constant(data_[i]));
    }
    return substitute(^^impl, elems);
}

We simply only serialize data[0:length] and not all of data[0:32]. The result is that the trailing data gets dropped and we end up with the same object:

constexpr auto f() -> SmallString {
    auto s = SmallString();
    s.push_back('x');
    return s;
}

constexpr auto g() -> SmallString {
    auto s = f();
    s.push_back('y');
    s.pop_back();
    return s;
}

static_assert(&ref<f()> == &ref<g()>);

// It doesn't matter that g()[1] == 'y', normalizing through the
// template parameter object gives us all trailing nulls
static_assert(ref<g()>[1] == '\0');

As a result, the example below just isn’t an ODR issue, because bad<f()> and bad<g()> both evaluate the same way — they return 1.

template <SmallString S>
constexpr auto bad() -> int {
  if constexpr (S.data()[1] == 'y') {
    return 0;
  } else {
    return 1;
  }
}

2.2 Defaulting operator template

While it’s important to be able to support the kind of arbitrary selection of serialization and normalization that I’ve shown in the previous three examples, in practice, for a large number of types, this kind of genericity is unnecessary. In fact, a lot of time, all we want to do is have a simple member-wise (or really subobject-wise) template-argument-equivalence.

That’s the C++20 rule we have today, except that rule requires that all the subobjects be public. That’s necessary, since we can’t otherwise distinguish between std::string_view and std::string, and those would definitely want to have very different rules around equivalence (the former comparing pointers, the latter comparing contents). But what if I want to both have private members and also have member-wise equivalence?

Take string_view. It’s straightforward enough to implement operator template in a way that does what we want:

template <class T, auto... Vals>
constexpr auto impl = T(Vals...);

class string_view {
    char const* ptr_;
    size_t len_;

public:
    constexpr string_view(char const*, size_t);

    consteval operator template() const {
        return substitute(^^impl, {
            ^^string_view,
            std::meta::reflect_constant(ptr_),
            std::meta::reflect_constant(len_),
        });
    }
};

This works fine, and we already have the constructor that we need to support this. But this is a lot of ceremony for what is basically the default behavior: serialize and then deserialize all of my members. Since that is a very reasonable default behavior, we should just allow default-ing this. This does mirror very nicely the way that other defaulted functions behave:

class string_view {
    char const* ptr_;
    size_t len_;

public:
    constexpr string_view(char const*, size_t);

    consteval operator template() const = default;
};

We’ll want defaulting behavior for operator template to be similar to defaulting behavior for other functions, like the copy constructor and the comparison operators: for dependent types, if one of the subobjects isn’t usable as a constant template parameter (whether it’s not C++20 structural or doesn’t customize operator template), the resulting customization is deleted rather than being ill-formed. That means that for a class template like std::tuple, we can simply default the implementation and not have to worry about providing suitable constraints.

And, like the copy constructor, operator template is implicitly declared. If a class doesn’t declare one, it gets one anyway: it is deleted if any subobject is not public or not structural, and defaulted otherwise. That lets the language define a structural class type simply as a literal type with no mutable subobjects whose operator template isn’t deleted. The only difference between the implicit declaration and writing = default yourself is the access check. Writing = default is how a class with private members says that member-wise equivalence is correct for it.

Note that a consequence of being able to default this function is that you should also be able to delete it, which means that a type could (if so desired) simply opt out of being usable as a constant template parameter (including types that would otherwise be usable by C++20 rules).

Also, the actual implementation details of defaulting operator template would differ from those of explicitly providing it. If it is user-provided, the implementation will of course invoke and use that result as the template parameter object. But if it is defaulted, then effectively the implementation just tracks an extra bit on the type that tells it to ignore the usual access rule for C++20 class types as non-type template parameters. It wouldn’t have to change the rules for template-argument-equivalence for that case.

2.3 Concrete Details

Currently, the rule is that f<x> and f<y> are the same specialization when x and y are template-argument-equivalent. For which the relevant rule from 13.6 Type equivalence [temp.type] for class types is:

2 Two values are template-argument-equivalent if they are of the same type and

  • (2.1) […]
  • (2.10) they are of union type and either they both have no active member or they have the same active member and their active members are template-argument-equivalent, or
  • (2.11) […], or
  • (2.12) they are of class type and their corresponding direct subobjects and reference members are template-argument-equivalent.

This proposal does not change those bullets. Instead, for a class type T (including union type) that provides a non-default operator template customization, f<x> and f<y> are the same specialization when x.operator template() == y.operator template() (i.e. they are reflections representing the same object). The template parameter object is the object returned by operator template, so template-argument-equivalence compares those objects, never x and y themselves. And because operator template must be deterministic and idempotent (see below), two objects returned by it have template-argument-equivalent values only if they are the same object, so the existing rules suffice.

In order for that to be viable, we need to impose strict (albeit obvious) restrictions on the return type of operator template. For a class type T, T::operator template must return a reflection representing either an object or a variable. Letting: O be either that object or the object defined by that variable:

operator template must also be deterministic (same value in, same object out) and idempotent (given a value x, x.operator template() and [: x.operator template() :].operator template() must be the same object — although note that this function isn’t directly invokable, I’m just presenting this as exposition). Idempotence we can actually check as part of the implementation (and is enforced in the wording), in the same way that template variable construction does an extra copy right now.

We’ll have the same rules for the shape of the customization point for defaulting reasons as we have for defaulting the comparison operators. That is, it must be:

Note that a type with mutable members is non-structural, regardless of any customization point.

2.4 Standard Library Extensions

There is one standard library type that I’m aware of that would actually provide a bespoke implementation of operator template (see below), but there are quite a few for whom defaulting is the correct behavior. For the class templates below, defaulting will do the right thing by opting in those types whose subobjects are usable as constant template parameters and not opting in those types whose subobjects are not usable (note that std::pair and std::array are absent from this list, but that is only because they are already usable as constant template parameters):

For all of these types and class templates, adding the single line:

consteval operator template() const = default;

Does the right thing and opts those types (or suitable specializations of those class templates) into being used as constant template parameters, with the correct semantics. Although this isn’t how it’s worded — the wording specifies the resulting semantics, not the spelling.

std::inplace_vector<T, N> is different: only the elements in [0, size()) should matter, as with SmallString above. An implementation could get that behavior with a custom operator template that looks the same as std::vector’s (defined below). It could also use a defaulted one, if the storage past size() consists of objects that are not within their lifetime. Which is better depends on how efficiently those holes can be mangled, which I think is still an open issue in Itanium. But when it comes to specification, we shouldn’t be concerned about how to implement it specifically — what we specify is the behavior: only [0, size()) participates.

2.5 Future Extensions with Containers

Notably absent from the above list are allocating containers, most significantly std::vector<T> and std::string. That’s because we do not yet have non-transient allocation and so cannot support them yet.

However, once we do have non-transient allocation, this paper provides a design that lets us easily provide constant template parameter support for them. Here is vector for instance:

template <class T, auto& R>
inline constexpr auto impl = T(std::from_range, R);

template <class T>
class vector {
public:
    consteval operator template() const
        requires std::meta::is_structural_type(^^T)
    {
        return substitute(^^impl, {
            ^^vector,
            std::meta::reflect_constant_array(*this),
        });
    }
};

Note that this implicitly normalizes capacity — as long as the elements are the same, we will choose the same specialization of impl, which means we will end up with the same object (which will have whatever capacity it has).

If for some reason, you actually do want capacity to participate in all of this, that’s easy too — just also pass in capacity() as an additional serialization argument.

2.6 String Literals

One other type to cover not only isn’t in the standard library, but it isn’t even a class type: string literals. While char const* is usable as a template parameter, we cannot currently pass a string literal as a template argument.

The reason for this is that we do not ensure that "hello" is the same exact pointer across all translation units (and we will not start doing this now), and the result is that given a class template like:

template <char const* P>
struct X { };

There is no guarantee that X<"hello"> is the same type in every translation unit.

Now, [P0424R2] (String literals as non-type template parameters) already proposed the solution to this problem. Except that paper was dropped in favor of the more general [P0732R2] (Class Types in Non-Type Template Parameters) in the C++20 timeframe. But we still cannot pass in string literals, so I’m resurrecting the ideas behind the original paper.

In modern C++26 reflection terms, the way we need this to work is that when we pass a string literal as a template argument:

X<"hello">

That it needs to evaluate instead as:

X<[: std::meta::reflect_constant_string("hello") :]>

And this now ensures that, across all translation units, X<"hello"> is the same type. This is really the only way to ensure this property, and is the requirement for this to be allowed to work, so we should do it.

Note that it’s not quite that simple, because:

constexpr char const* a = "holdup";
constexpr char const* b = a + 3;
constexpr char const* c = "dup";

X<b> dup1;
X<c> dup2;

Do dup1 and dup2 have the same type? I would argue no, they do not. The transformation I expect is more like this:

template <char... Cs>
inline constexpr char const lit[] = {Cs..., '\0'};

X<lit<'h', 'o', 'l', 'd', 'u', 'p'> + 3> dup1;
X<lit<'d', 'u', 'p'>> dup2;

That is, just because the string contents of a + 3 and c are the same ("dup"), the pointers themselves are different, and template-argument-equivalence should preserve that. This is essential for another issue to bring up…

2.7 Normalization II

I talked about normalization earlier from the perspective of wanting to mutate an object down to its canonical representation, and only use that canonical representation as a template parameter object.

But there’s another perspective of normalization to consider: simple aggregation. What happens here:

struct Stuff {
    std::string_view s;
    int x;
};

template <Stuff S> int f();

int r = f<Stuff{.s="hello"sv, .x=1}>();

Stuff has all-public members, all of whom are usable as constant template parameters, so this should just work without Stuff having to do anything. But in order for this to work:

This all needs to basically… just happen as part of the template parameter object construction process. Otherwise composition completely breaks down.

This is also why the string literal rule has to have the form I described above. Imagine if std::string_view’s storage looked like this:

class string_view {
    char const* begin_;
    char const* end_;
};

In the above initialization, string_view("hello"sv) would have two pointers into the same string literal — so that needs to transform into (p, p + 5) where p points to a static storage duration array. It cannot turn into two pointers into different arrays. That would be horribly broken.

2.8 Naming

In [P4340R0], the name I chose for the customization point was reflect_constant, since it was conceptually customizing std::meta::reflect_constant. There was precedent for using regular identifiers in language customization points already (begin, end, get, plus all the coroutine stuff), so this seemed alright. Although with = default, it’s possible that this was a bit too much.

However, there are some real issues with attempting to use the name reflect_constant that would be improved by simply… not using a real identifier:

Instead, this revision changes the name of the customization point to operator template() — the name proposed in the original paper in the constant template parameter trilogy, [P2484R0] (Extending class types as non-type template parameters). Richard Smith had also suggested operator<>(), but that seems more cryptic, and plus operator< <> is technically valid, so I don’t think we need more punctuation here. operator template is a pretty good name, since this is the operator that turns our value into a template arguemnt. It fits. The spelling also lets us avoid a return type, like conversion functions, so it’s just less to type too.

Now, in that original discussion, some people didn’t like that operator template() wasn’t directly invokable, but we now have a way of getting that result directly anyway via std::meta::reflect_constant. So we’re not missing any functionality, and restricting the use of operator template just prevents misuses.

2.9 Implementation Experience

This has been implemented in my fork of Clang (updated for R1 in this commit), and you can see it on compiler explorer.

3 Wording

The wording introduces a template-argument function: a special member function named operator template, specified in a new subclause [class.template.arg]. Like a copy constructor, it is declared implicitly if a class doesn’t declare one, and its implicit declaration and defaulted definition encode the C++20 requirements on structural class types. A structural class type is then simply a literal class type with no mutable subobjects that has an eligible template-argument function. Normalization, both of string literals and of values whose type has a user-provided template-argument function, happens when the value of a constant template parameter is determined. Finally, the wording goes through all of the standard library types in the list and makes them suitably structural based on their semantics, not on any particular spelling of operator template.

3.1 Language

A template-argument function, like a conversion function, has no type in its decl-specifier-seq. Change 9.2.9.1 General [dcl.type.general]/3:

3 Except in a declaration of a constructor, destructor, or conversion function, or template-argument function ([class.template.arg]), at least one defining-type-specifier that is not a cv-qualifier shall appear in a complete type-specifier-seq or a complete decl-specifier-seq.

Change 11.4.4 Special member functions [special]/1, /3, and /5:

1 Default constructors ([class.default.ctor]), copy constructors, move constructors ([class.copy.ctor]), copy assignment operators, move assignment operators ([class.copy.assign]), and prospective destructors ([class.dtor]), and template-argument functions ([class.template.arg]) are special member functions. […]

3 [ Note: The special member functions affect the way objects of class type are created, copied, moved, and destroyed, and how values can be converted to values of other types, and how values are used as constant template arguments. Often such special member functions are called implicitly. — end note ]

5 Two special member functions are of the same kind if

  • (5.1) they are both default constructors,
  • (5.2) they are both copy or move constructors with the same first parameter type, or
  • (5.3) they are both copy or move assignment operators with the same first parameter type and the same cv-qualifiers and ref-qualifier, if any., or
  • (5.4) they are both template-argument functions.

Change 9.6.2 Explicitly-defaulted functions [dcl.fct.def.default]/2 and /5:

2 The program is ill-formed if an explicitly defaulted special member function F1 of class C differs from the corresponding special member function F2 that would have been implicitly declared other than as follows:

  • (2.1) if F1 is an assignment operator, it may have the & ref-qualifier;
  • (2.2) […]
  • (2.3) F1 and F2 may have differing exception specifications;
  • (2.4) […]; and
  • (2.5) if F2 has a non-object parameter of type “lvalue reference to C”, the corresponding non-object parameter of F1 may be of type “lvalue reference to const C”; in this case only, F1 is defined as deleted if it is explicitly defaulted on its first declaration and the program is ill-formed otherwise.; and
  • (2.6) if F2 is a template-argument function, F1 may have an explicit object parameter of type C or of type “lvalue reference to const C”, in which case the type of F1 would differ from the type of F2 in that the type of F1 has an additional parameter.

3 […]

4 […]

5 Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions, and the implementation shall provide implicit definitions for them ([class.ctor], [class.dtor], [class.copy.ctor], [class.copy.assign], [class.template.arg]) as described below, including possibly defining them as deleted. […]

Add template to the grammar of operator in 12.4.1 General [over.oper.general]/1, and add a paragraph after 12.4.1 General [over.oper.general]/6:

1 A declaration whose declarator-id is an operator-function-id shall declare a function or function template or an explicit instantiation or specialization of a function template. A function so declared is an operator function. […]

operator-function-id:
    operator operator

operator: one of
    new      delete   new[]    delete[] co_await ()       []       ->       ->*
    ~        !        +        -        *        /        %        ^        &
    |        =        +=       -=       *=       /=       %=       ^=       &=
    |=       ==       !=       <        >        <=       >=       <=>      &&
    ||       <<       >>       <<=      >>=      ++       --       ,        template

6 The co_await operator is described completely in [expr.await]. The attributes and restrictions found in the rest of [over.oper] do not apply to it unless explicitly stated in [expr.await].

6+ The template-argument function, operator template, is described completely in [class.template.arg]. The attributes and restrictions found in the rest of [over.oper] do not apply to it unless explicitly stated in [class.template.arg].

Add a new subclause after 11.4.7 Destructors [class.dtor]:

Template-argument functions [class.template.arg]

1 A template-argument function of a class C is a non-static member function of C whose name is the operator-function-id operator template and whose object parameter has type const C& or C, other than a function template or a specialization of one. The declarator of a template-argument function shall consist only of its declarator-id followed by its parameters-and-qualifiers. The operator-function-id operator template shall be used only in the declarator-id of a declaration of a template-argument function. [ Note 1: For example, it cannot be used in an expression, as the operand of a reflect-expression, in a using-declarator, or in a template-id. — end note ]

2 The declared return type of a template-argument function is std::meta::info. No decl-specifier in its decl-specifier-seq shall be a defining-type-specifier. A user-declared template-argument function shall be declared consteval and shall have no non-object parameters.

3 A template-argument function shall not be named by an expression ([basic.def.odr]). [ Note 2: A template-argument function is invoked only as specified in this subclause, including by std::meta::reflect_constant ([meta.reflection.result]). It cannot be called or have its address taken, even by way of a splice-expression, but std::meta::members_of can represent it ([meta.reflection.member.queries]). — end note ]

4 If the class is not an anonymous union and does not have a user-declared template-argument function, one is declared implicitly. If the class has a direct base class or a direct non-static data member that is not public, the implicitly-declared template-argument function is defined as deleted; otherwise, it is defaulted ([dcl.fct.def]). The implicitly-declared template-argument function for a class C is an inline public member of C and has the form

consteval C::operator template() const

[ Note 3: An explicitly-defaulted template-argument function differs from an implicitly-declared one in that access is not considered: it is not defined as deleted because of a non-public direct base class or non-static data member. — end note ]

5 A class shall not have more than one eligible ([special]) template-argument function.

6 A template-argument function that is defined as defaulted shall be defined as defaulted on its first declaration.

7 Let C be a class whose eligible template-argument function F is user-provided. When the template parameter object for an object x of type cv C is determined ([temp.arg.nontype]), F is invoked with x as its object argument, without regard to access control; let R be the result of that invocation. If the template parameter object is determined as part of an evaluation X during the evaluation of an expression as a core constant expression (for example, by a call to std::meta::reflect_constant), the invocation is evaluated as part of X. Otherwise, the invocation is evaluated as a manifestly constant-evaluated expression that appears at the point at which the template argument from which the value of the constant template parameter is being determined ([temp.arg.nontype]) appears. [ Note 4: This determines the evaluation context ([expr.const.reflect]) of the invocation, and so the behavior of functions used for reflection ([meta.reflection]) during it. — end note ] The program is ill-formed if that invocation is not a constant expression ([expr.const]). Otherwise, the program is ill-formed unless R represents either an object or a variable; let O be that object or the object declared by that variable. The program is ill-formed unless

  • (7.1) O is the object declared by a variable with static storage duration, or a subobject of such an object,
  • (7.2) O is usable in constant expressions,
  • (7.3) O is a const object ([basic.type.qualifier]) of type C or const C,
  • (7.4) the complete object of O ([intro.object]) is not TU-local ([basic.link]) unless x is TU-local, and
  • (7.5) the invocation of F with O as its object argument is a constant expression whose result represents either O or a variable that declares O.

O is the template parameter object ([temp.param]) for x.

8 For any two objects x and y of a class type C whose eligible template-argument function is user-provided, if the corresponding direct subobjects and reference members of x and y are template-argument-equivalent ([temp.type]), the template parameter object for x shall be the same object as the template parameter object for y; no diagnostic required. [ Note 5: A user-provided template-argument function need not be injective: objects whose subobjects are not template-argument-equivalent can have the same template parameter object. — end note ]

9 A defaulted template-argument function of a class C is defined as deleted if any direct base class of C, any non-static data member of C other than an anonymous union member, or any variant member of C has a type that is neither a structural type ([temp.param]) nor a (possibly multidimensional) array of structural types.

10

[ Example 1:
struct A {                                     // A::operator template is implicitly declared
  int i;                                       // and defaulted
};

class B {                                      // B::operator template is implicitly declared
  int i;                                       // and defined as deleted: B::i is not public
};

class C {
  int i;
public:
  consteval operator template() const = default;  // OK, access is not considered
};

struct D {
  consteval operator template() const = default;  // defined as deleted: B is not a structural type
private:
  B b;
};

struct E : C { };                              // E::operator template is implicitly declared
                                               // and defaulted; C is a structural type

struct F {                                     // F::operator template is implicitly declared
  union { int i; float f; };                   // and defaulted; the anonymous union's members
};                                             // are variant members of F

struct Mod {                                   // values are equivalent modulo 7
  int v;
  consteval operator template() const;
};
template<int V> constexpr Mod canonical = {V};
consteval Mod::operator template() const {
  return substitute(^^canonical, {std::meta::reflect_constant((v % 7 + 7) % 7)});
}

template<Mod m> struct X { };
static_assert(std::is_same_v<X<Mod{3}>, X<Mod{10}>>);  // OK, the template parameter object
                                                       // for both is canonical<3>

struct Bad {
  int v;
  consteval operator template() const { return ^^canonical<0>; }
};
template<Bad b> struct Y { };
Y<Bad{1}> y;                                   // error: canonical<0> has type const Mod,
                                               // not const Bad

struct Year {                                  // the template parameter object is a subobject
  int y;                                       // of the variable jan1<y>
  consteval operator template() const;
};
struct Date { Year year; int month, day; };
template<int N> constexpr Date jan1 = {{N}, 1, 1};
consteval Year::operator template() const {
  auto r = substitute(^^jan1, {std::meta::reflect_constant(y)});
  return std::meta::reflect_object(std::meta::extract<const Date&>(r).year);
}
template<Year y> constexpr const Year* addr = &y;
static_assert(addr<Year{2024}> == &jan1<2024>.year);

struct G {
  operator template() const;                   // error: not consteval
};

struct H {
  consteval operator template();               // error: object parameter has type H&
};

struct I {
  consteval std::meta::info operator template() const;  // error: return type specified
};

struct J {
  consteval operator template() const;
};
consteval J::operator template() const = default;  // error: not defaulted on first declaration

struct K : A {
  using A::operator template;                  // error: operator template named by a using-declarator
};

consteval std::meta::info f(A a) {
  return a.operator template();                // error: operator template named in an expression
}
constexpr std::meta::info r = ^^A::operator template;  // error: operator template named in an expression
constexpr std::meta::info s = std::meta::reflect_constant(A{1});  // OK
— end example ]

Change the definition of structural type in 13.2 Template parameters [temp.param]/12:

12 A structural type is one of the following:

  • (12.1) a scalar type, or
  • (12.2) an lvalue reference type, or
  • (12.3) a literal class type with the following properties: that has no mutable subobjects and has an eligible template-argument function ([class.template.arg]).
    • (12.3.1) all base classes and non-static data members are public and non-mutable and
    • (12.3.2) the types of all base classes and non-static data members are structural types or (possibly multidimensional) arrays thereof.

Change 13.2 Template parameters [temp.param]/13. A template parameter object of a class type whose eligible template-argument function is user-provided can be the object declared by a variable, or a subobject of such an object, so not all template parameter objects are distinct from all other objects. Also, a template parameter object is template-argument-equivalent to the normalized template argument, not to the template argument as written:

13 Certain constructs refer to template parameter objects, which are distinct objects with static storage duration and non-volatile const type. A template parameter object of a class type whose eligible template-argument function is user-provided is determined by that function ([class.template.arg]) and can be the object declared by a variable or a subobject of such an object; other template parameter objects are distinct from all other objects. No two such template parameter objects have template-argument-equivalent values ([temp.type]). An id-expression naming a constant template parameter of class type T denotes the template parameter object of type const T, which is template-argument-equivalent to the corresponding template argument after it has been converted to the type of the template parameter and normalized ([temp.arg.nontype]).

Change 13.4.3 Constant template arguments [temp.arg.nontype]/2-5. Paragraphs 4 and 5 become bullets of the new paragraph that introduces v; their text is otherwise unchanged:

2 The value of a constant template parameter P of (possibly deduced) type T is determined from its template argument A as follows. If T is not a class type and A is not a braced-init-list, A shall be a converted constant expression ([expr.const.const]) of type T; the value of P is A (as converted) the normalized value (see below) of A (as converted).

3 Otherwise, a temporary variable

- constexpr T v = A;
+ constexpr T u = A;

is introduced. The lifetime of v ends immediately after initializing it and any template parameter object (see below). For each such variable, the id-expression v is termed a candidate initializer.

a If T is a class type whose eligible template-argument function is user-provided, P denotes the template parameter object ([class.template.arg]) for u. The lifetime of u ends immediately after that template parameter object is determined. [ Note: The template parameter object is not initialized from u. — end note ]

b Otherwise, a temporary variable v of type const T is introduced whose value is the normalized value (see below) of u. The lifetimes of u and v end immediately after initializing v and any template parameter object (see below). For each such variable v, the id-expression v is termed a candidate initializer.

  • (b.1) If T is a class type, a template parameter object ([temp.param]) exists that is constructed so as to be template-argument-equivalent to v; P denotes that template parameter object. P is copy-initialized from an unspecified candidate initializer that is template-argument-equivalent to v. If, for the initialization from any candidate initializer,
    • (b.1.1) the initialization would be ill-formed, or
    • (b.1.2) the full-expression of an invented init-declarator for the initialization would not be a constant expression when interpreted as a constant-expression ([expr.const.const]), or
    • (b.1.3) the initialization would cause P to not be template-argument-equivalent ([temp.type]) to v,
    the program is ill-formed.
  • (b.2) Otherwise, the value of P is that of v.

c For a string literal object S ([lex.string]), let PS be the template parameter object of the same type as S whose elements are template-argument-equivalent to the corresponding elements of S. [ Note: PS is the object represented by std::meta::reflect_constant_string(S) ([meta.define.static]). It is a potentially non-unique object ([intro.object]). — end note ]

d The normalized value of an object, value, or reference x of type T is:

  • (d.1) If T is a pointer type and x points to or past the end of an object o that is a string literal object S or a subobject of S, the pointer value that points to or past the end of, respectively, the object that corresponds to o within PS.
  • (d.2) Otherwise, if T is a reference type and x refers to an object o that is a string literal object S or a subobject of S, a reference to the object that corresponds to o within PS.
  • (d.3) Otherwise, if T is an array type, the array value whose elements are the normalized values of the corresponding elements of x.
  • (d.4) Otherwise, if T is a class type whose eligible template-argument function ([class.template.arg]) is user-provided, the value of an object of type T copy-initialized from an lvalue designating the template parameter object for x ([class.template.arg]). The program is ill-formed if that copy-initialization would be ill-formed or if the full-expression of an invented init-declarator for it would not be a constant expression.
  • (d.5) Otherwise, if T is a union type, x if x has no active member; otherwise, the value that has the same active member as x, whose value is the normalized value of the active member of x.
  • (d.6) Otherwise, if T is a class type, the value whose direct subobjects and reference members have the normalized values of the corresponding direct subobjects and reference members of x.
  • (d.7) Otherwise, x.

[ Note: Normalization replaces each pointer into, or reference to, a string literal object or subobject thereof with a pointer into, or reference to, an object whose identity is the same in every translation unit, and replaces each subobject of a class type whose eligible template-argument function is user-provided with a copy of the template parameter object for that subobject. — end note ]

[ Example:
template<const char* p> struct X { };
X<"hello"> x1;
X<std::define_static_string("hello")> x2;    // same type as x1

constexpr const char* a = "holdup";
X<a + 3> x3;
X<"dup"> x4;                                // not the same type as x3

template<const char& r> struct W { };
W<"hello"[1]> w1;
W<std::define_static_string("hello")[1]> w2;  // same type as w1

struct Frac {
  int num, den;
  consteval operator template() const;
};
template<int N, int D> constexpr Frac interned = {N, D};
consteval Frac::operator template() const {
  int g = std::gcd(num, den);
  return substitute(^^interned, {std::meta::reflect_constant(num / g),
                                 std::meta::reflect_constant(den / g)});
}

template<Frac F> struct Y { };
Y<Frac{2, 4}> y1;                           // the template parameter object is interned<1, 2>
Y<interned<3, 6>> y2;                       // same type as y1

struct Stuff {
  Frac f;
  const char* s;
};
template<Stuff S> struct Z { };
Z<Stuff{{2, 4}, "hi"}> z1;
Z<Stuff{{1, 2}, std::define_static_string("hi")}> z2;   // same type as z1
— end example ]

Remove 13.4.3 Constant template arguments [temp.arg.nontype]/6.2. By the time this check applies, pointers into and references to string literal objects have been normalized. A template parameter object determined by a user-provided template-argument function is not normalized, but it is an ordinary variable, no different from any other object a reference template parameter can refer to:

6 For a constant template parameter of reference or pointer type, or for each non-static data member of reference or pointer type in a constant template parameter of class type or subobject thereof, the reference or pointer value shall not refer or point to (respectively):

Change the note in 13.4.3 Constant template arguments [temp.arg.nontype]/9:

9

[ Note: A string-literal ([lex.string]) is not an acceptable template-argument for a constant template parameter of non-class pointer or reference type; the resulting pointer value or reference is normalized to point into or refer to a template parameter object.

[ Example:
  template<class T, T p> class X {
    ...
  };

- X<const char*, "Studebaker"> x; // error: string literal object as template-argument
- X<const char*, "Knope" + 1> x2; // error: subobject of string literal object as template-argument
+ X<const char*, "Studebaker"> x; // OK
+ X<const char*, "Knope" + 1> x2; // OK
+ X<const char*, "Knope" + 1> x3; // OK, same type as x2

  const char p[] = "Vivisectionist";
  X<const char*, p> y;            // OK

  struct A {
    constexpr A(const char*) {}
  };

  X<A, "Pyrophoricity"> z;        // OK, string-literal is a constructor argument to A
— end example ]
— end note ]

[ Drafting note: 13.6 Type equivalence [temp.type] needs no change. Every value compared there has already been normalized. For a class type whose eligible template-argument function is user-provided, determinism ([class.template.arg]/8) and idempotence ([class.template.arg]/7.5) mean that two template parameter objects have template-argument-equivalent values exactly when they are the same object. ]

[ Drafting note: 6.7 Program and linkage [basic.link] needs no change. The template argument for a constant template parameter of class type whose eligible template-argument function is user-provided designates its template parameter object, which can be the object declared by a variable or a subobject of such an object. [class.template.arg]/7.4 ensures that the complete object is TU-local only when the template argument already was. ]

3.2 Reflection library

is_special_member_function (21.4.7 Reflection queries [meta.reflection.queries]) is specified in terms of 11.4.4 Special member functions [special], so it returns true for a template-argument function without any change. The implicitly-declared template-argument function is returned by members_of, so update the counts in the example in 21.4.11 Reflection member queries [meta.reflection.member.queries]:

  class B {};

  struct S : B {
  private:
    class I;
  public:
    int m;
  };

- static_assert(                                                  // 6 special members,
-   members_of(^^S, access_context::current()).size() == 7);      // 1 public member,
+ static_assert(                                                  // 7 special members,
+   members_of(^^S, access_context::current()).size() == 8);      // 1 public member,
                                                                  // does not include base

  static_assert(                                                  // all of the above,
-   members_of(^^S, access_context::unchecked()).size() == 8);    // as well as a reflection
+   members_of(^^S, access_context::unchecked()).size() == 9);    // as well as a reflection
                                                                  // representing S::I

Add one row, for op_template, to the end of the table of the enumerators of operators (Table [tab:meta.reflection.operators]) in 21.4.5 Operator representations [meta.reflection.operators]. The existing rows are unchanged:

Constant
Corresponding operator-function-id
Operator symbol name
[…] […] […]
op_plus_plus operator++ ++
op_minus_minus operator-- --
op_comma operator, ,
op_template operator template template

A template-argument function cannot be named by an expression ([class.template.arg]/3), so extract must not produce a pointer to one. Change 21.4.14 Value extraction [meta.reflection.extract]/7:

template<class T>
  consteval T extract-member-or-function(info r);       // exposition only

7 Throws: meta::exception unless

  • (7.1) r represents a non-static data member with type X, that is not a bit-field, that is a direct member of class C, T and X C::* are similar types ([conv.qual]), and is_convertible_v<X C::*, T> is true;
  • (7.2) r represents an implicit object member function that is not a template-argument function ([class.template.arg]) with type F or F noexcept that is a direct member of a class C, and T is F C::*; or
  • (7.3) r represents a non-member function, static member function, or explicit object member function that is not a template-argument function of function type F or F noexcept, and T is F*.

Change the specification of template_arguments_of in 21.4.7 Reflection queries [meta.reflection.queries]:

consteval vector<info> template_arguments_of(info r);

59 Returns: A vector containing reflections of the template arguments of the template specialization represented by r, in the order in which they appear in the corresponding template argument list. For a given template argument A, its corresponding reflection R is determined as follows:

  • (59.1) If A denotes a type or type alias, […]
  • (59.2) Otherwise, if A denotes a class template, variable template, concept, or alias template, […]
  • (59.3) Otherwise, A is a constant template argument ([temp.arg.nontype]). Let P be the corresponding template parameter.
    • (59.3.1) If P has reference type, then R is a reflection representing the object or function referred to by A P as determined from A ([temp.arg.nontype]).
    • (59.3.2) Otherwise, if P has class type, then R represents the corresponding template parameter object.
    • (59.3.3) Otherwise, R is a reflection representing the value of A P as determined from A ([temp.arg.nontype]).

std::meta::reflect_constant is specified in terms of template_arguments_of(^^TCls<V>)[0], so it picks up both the user-provided template-argument function and normalization from the core wording. What remains is to update the example in 21.4.16 Expression result reflection [meta.reflection.result]:

template<class T>
  consteval info reflect_constant(T expr);

1 Mandates: is_copy_constructible_v<T> is true and T is a cv-unqualified structural type ([temp.param]) that is not a reference type.

2 Let V be:

  • (2.1) if T is a class type, then an object that is template-argument-equivalent to the value of expr;
  • (2.2) otherwise, the value of expr.

3 Let TCls be the invented template:

template<T P> struct TCls;

4 Returns: template_arguments_of(^^TCls<V>)[0]. [ Note: This is a reflection of an object for class types, and a reflection of a value otherwise. — end note ]

5 Throws: meta::exception unless the template-id TCls<V> would be valid.

6

[ Example:
  template<auto D>
    struct A { };

  struct N { int x; };
  struct K { char const* p; };

  constexpr info r1 = reflect_constant(42);
  static_assert(is_value(r1));
  static_assert(r1 == template_arguments_of(^^A<42>)[0]);

  constexpr info r2 = reflect_constant(N{42});
  static_assert(is_object(r2));
  static_assert(r2 == template_arguments_of(^^A<N{42}>)[0]);

  constexpr info r3 = reflect_constant(K{nullptr});
- constexpr info r4 = reflect_constant(K{"ebab"});        // error: constituent pointer
-                                                         // points to string literal
+ constexpr info r4 = reflect_constant(K{"ebab"});        // OK
+ static_assert(extract<K>(r4).p == define_static_string("ebab"));
+
+ struct Frac {
+   int num, den;
+   consteval operator template() const;
+ };
+ template<int N, int D> constexpr Frac interned = {N, D};
+ consteval Frac::operator template() const {
+   int g = std::gcd(num, den);
+   return substitute(^^interned, {std::meta::reflect_constant(num / g),
+                                  std::meta::reflect_constant(den / g)});
+ }
+
+ constexpr info r5 = reflect_constant(Frac{2, 4});
+ static_assert(is_object(r5));
+ static_assert(r5 == reflect_object(interned<1, 2>));
+ static_assert(r5 == template_arguments_of(^^A<Frac{3, 6}>)[0]);
— end example ]

3.3 Standard library types

[ Drafting note: Every class now has a template-argument function. For a standard library class, whether that function is implicitly declared, explicitly defaulted, or user-provided should probably be unspecified, as should its exact signature. Users get at this functionality through std::meta::reflect_constant, and implementations may need to explicitly default the function on internal helper types as well. This probably needs a sentence in 16.4.6.5 Member functions [member.functions]. ]

Following the existing wording for pair (22.3.2 Class template pair [pairs.pair]) and array (23.3.3.1 Overview [array.overview]), the library types are specified by stating when they are structural types and what template-argument-equivalence means for them, rather than by adding a defaulted operator template to every synopsis. Explicitly defaulting only exempts the direct subobjects from the access requirement ([class.template.arg]/4, /9), so implementations will also need to opt in their internal storage types, and this approach leaves them free to do so. inplace_vector is specified slightly differently (see below).

3.3.1 optional

Add a paragraph to the end of 22.5.3.1 General [optional.optional.general]:

3 optional<T> is a structural type ([term.structural.type]) if T is a structural type. Two values x and y of type optional<T> are template-argument-equivalent ([temp.type]) if and only if either x.has_value() and y.has_value() are both false, or they are both true and x.val and y.val are template-argument-equivalent.

Add a paragraph to the end of 22.5.4.1 General [optional.optional.ref.general]:

3 optional<T&> is a structural type ([term.structural.type]). Two values x and y of type optional<T&> are template-argument-equivalent ([temp.type]) if and only if either x.has_value() and y.has_value() are both false, or they are both true and *x and *y refer to the same object.

3.3.2 variant

Add a paragraph to the end of 22.6.3.1 General [variant.variant.general]:

5 variant<Types...> is a structural type ([term.structural.type]) if each type in Types is a structural type. Two values x and y of type variant<Types...> are template-argument-equivalent ([temp.type]) if and only if x.index() == y.index() is true and either x.valueless_by_exception() is true or the objects referred to by get<I>(x) and get<I>(y) are template-argument-equivalent, where I is x.index().

3.3.3 tuple

Add a paragraph to the end of 22.4.4.1 General [tuple.tuple.general]:

2 tuple<Types...> is a structural type ([term.structural.type]) if each type in Types is a structural type. Two values x and y of type tuple<Types...> are template-argument-equivalent ([temp.type]) if and only if, for every I in the range [0, sizeof...(Types)):

  • (2.1) if Types...[I] is a reference type, then get<I>(x) and get<I>(y) refer to the same object;
  • (2.2) otherwise, the objects referenced by get<I>(x) and get<I>(y) are template-argument-equivalent.

3.3.4 expected

Add a paragraph to the end of 22.8.6.1 General [expected.object.general]:

4 expected<T, E> is a structural type ([term.structural.type]) if T and E are both structural types. Two values x and y of type expected<T, E> are template-argument-equivalent ([temp.type]) if and only if either x.has_value() and y.has_value() are both true and x.val and y.val are template-argument-equivalent, or they are both false and x.unex and y.unex are template-argument-equivalent.

Add a paragraph to the end of 22.8.7.1 General [expected.void.general]:

4 expected<T, E> is a structural type ([term.structural.type]) if E is a structural type. Two values x and y of type expected<T, E> are template-argument-equivalent ([temp.type]) if and only if either x.has_value() and y.has_value() are both true, or they are both false and x.unex and y.unex are template-argument-equivalent.

3.3.5 reference_wrapper

Add a paragraph to the end of 22.10.6.1 General [refwrap.general]:

4 reference_wrapper<T> is a structural type ([term.structural.type]). Two values x and y of type reference_wrapper<T> are template-argument-equivalent ([temp.type]) if and only if x.get() and y.get() refer to the same object or function.

3.3.6 Comparison categories

Add a paragraph to the end of 17.12.2.1 Preamble [cmp.categories.pre]:

5 The types partial_ordering, weak_ordering, and strong_ordering are structural types ([term.structural.type]). Two values x and y of the same such type are template-argument-equivalent ([temp.type]) if and only if x == y is true.

3.3.7 bitset

Add a paragraph to the end of 22.9.2.1 General [template.bitset.general]:

14 bitset<N> is a structural type ([term.structural.type]). Two values x and y of type bitset<N> are template-argument-equivalent ([temp.type]) if and only if x == y is true.

3.3.8 basic_string_view

Add a paragraph to the end of 27.3.3.1 General [string.view.template.general]:

5 basic_string_view<charT, traits> is a structural type ([term.structural.type]). Two values x and y of type basic_string_view<charT, traits> are template-argument-equivalent ([temp.type]) if and only if x.data() and y.data() are template-argument-equivalent and x.size() == y.size() is true.

3.3.9 span

Add a paragraph to the end of 23.7.2.2.1 Overview [span.overview]:

6 span<ElementType, Extent> is a structural type ([term.structural.type]). Two values x and y of type span<ElementType, Extent> are template-argument-equivalent ([temp.type]) if and only if x.data() and y.data() are template-argument-equivalent and x.size() == y.size() is true.

3.3.10 inplace_vector

For inplace_vector, only the elements in the range [0, size()) are part of its value. The elements past size() must not affect which template parameter object a template argument names. There are two ways an implementation can get that behavior:

The choice depends on mangling. With a defaulted function, the mangled name of a specialization encodes the template parameter object’s entire storage, including the holes past size(). Whether that’s acceptable depends on how compactly those holes can be mangled. With a user-provided function, the mangled name refers to the returned object instead, and that object’s own name encodes only the serialized elements. That is an implementation decision, so the wording specifies only the behavior. It doesn’t state when two inplace_vector values are template-argument-equivalent, because with a user-provided function that relation compares the returned objects, so it isn’t meaningful for arbitrary values. Instead, it says when two template arguments determine the same template parameter object, which holds for either implementation.

Add a paragraph to the end of 23.3.16.1 Overview [inplace.vector.overview]:

6 inplace_vector<T, N> is a structural type ([term.structural.type]) if T is a structural type. Two constant template arguments of type inplace_vector<T, N> whose values are x and y determine the same template parameter object ([temp.arg.nontype]) if and only if x.size() == y.size() is true and the objects referred to by x[i] and y[i] are template-argument-equivalent for each i in the range [0, x.size()).

3.3.11 complex

Add a paragraph to the end of 29.4.3 Class template complex [complex]:

2 complex<T> is a structural type ([term.structural.type]). Two values x and y of type complex<T> are template-argument-equivalent ([temp.type]) if and only if x.real() and y.real() are template-argument-equivalent and x.imag() and y.imag() are template-argument-equivalent.

3.3.12 <chrono>

Add a paragraph to the end of 30.5.1 General [time.duration.general]:

7 duration<Rep, Period> is a structural type ([term.structural.type]) if Rep is a structural type. Two values x and y of type duration<Rep, Period> are template-argument-equivalent ([temp.type]) if and only if x.count() and y.count() are template-argument-equivalent.

Add a paragraph to the end of 30.6.1 General [time.point.general]:

2 time_point<Clock, Duration> is a structural type ([term.structural.type]) if Duration is a structural type. Two values x and y of type time_point<Clock, Duration> are template-argument-equivalent ([temp.type]) if and only if x.time_since_epoch() and y.time_since_epoch() are template-argument-equivalent.

Add a paragraph to the end of 30.8.3.1 Overview [time.cal.day.overview]:

3 day is a structural type ([term.structural.type]). Two values x and y of type day are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.4.1 Overview [time.cal.month.overview]:

3 month is a structural type ([term.structural.type]). Two values x and y of type month are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.5.1 Overview [time.cal.year.overview]:

3 year is a structural type ([term.structural.type]). Two values x and y of type year are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.6.1 Overview [time.cal.wd.overview]:

3 weekday is a structural type ([term.structural.type]). Two values x and y of type weekday are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.7.1 Overview [time.cal.wdidx.overview]:

4 weekday_indexed is a structural type ([term.structural.type]). Two values x and y of type weekday_indexed are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.8.1 Overview [time.cal.wdlast.overview]:

4 weekday_last is a structural type ([term.structural.type]). Two values x and y of type weekday_last are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.9.1 Overview [time.cal.md.overview]:

3 month_day is a structural type ([term.structural.type]). Two values x and y of type month_day are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph after paragraph 3 of 30.8.10 Class month_day_last [time.cal.mdlast]:

a month_day_last is a structural type ([term.structural.type]). Two values x and y of type month_day_last are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.11.1 Overview [time.cal.mwd.overview]:

4 month_weekday is a structural type ([term.structural.type]). Two values x and y of type month_weekday are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.12.1 Overview [time.cal.mwdlast.overview]:

4 month_weekday_last is a structural type ([term.structural.type]). Two values x and y of type month_weekday_last are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.13.1 Overview [time.cal.ym.overview]:

3 year_month is a structural type ([term.structural.type]). Two values x and y of type year_month are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.14.1 Overview [time.cal.ymd.overview]:

3 year_month_day is a structural type ([term.structural.type]). Two values x and y of type year_month_day are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.15.1 Overview [time.cal.ymdlast.overview]:

3 year_month_day_last is a structural type ([term.structural.type]). Two values x and y of type year_month_day_last are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.16.1 Overview [time.cal.ymwd.overview]:

3 year_month_weekday is a structural type ([term.structural.type]). Two values x and y of type year_month_weekday are template-argument-equivalent ([temp.type]) if and only if x == y is true.

Add a paragraph to the end of 30.8.17.1 Overview [time.cal.ymwdlast.overview]:

3 year_month_weekday_last is a structural type ([term.structural.type]). Two values x and y of type year_month_weekday_last are template-argument-equivalent ([temp.type]) if and only if x == y is true.

3.4 Feature-test macros

Bump __cpp_nontype_template_args in 15.12 Predefined macro names [cpp.predefined]:

- __cpp_nontype_template_args 201911L
+ __cpp_nontype_template_args 2026XXL

Add a new library feature-test macro to 17.3.2 Header <version> synopsis [version.syn] for the library types above. The name is a placeholder:

+ #define __cpp_lib_constant_template_args 2026XXL // also in <optional>, <expected>, <variant>, <tuple>, <string_view>, <span>, <functional>, <complex>, <compare>, <bitset>, <inplace_vector>, <chrono>

4 Acknowledgements

Thanks to Tim Song as usual for discussing all the design issues and for helping to make the paper better. Thanks to Dan Katz for an example that was deliberately left out of this paper, for everyone’s safety. Thanks to Tomasz Kamiński for help with the wording and discussions of non-uniquness and allowing a template parameter object to be a subobject.

5 References

[P0424R2] Louis Dionne, Hana Dusíková. 2017-11-14. String literals as non-type template parameters.
https://wg21.link/p0424r2
[P0732R2] Jeff Snyder, Louis Dionne. 2018-06-06. Class Types in Non-Type Template Parameters.
https://wg21.link/p0732r2
[P2484R0] Richard Smith. 2021-11-17. Extending class types as non-type template parameters.
https://wg21.link/p2484r0
[P2996R13] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, and Dan Katz. 2025-06-18. Reflection for C++26.
https://wg21.link/p2996r13
[P3380R1] Barry Revzin. 2024-12-17. Extending support for class types as non-type template parameters.
https://wg21.link/p3380r1
[P3491R3] Wyatt Childers, Peter Dimov, Barry Revzin, and Daveed Vandevoorde. 2025-06-20. define_static_{string,object,array}.
https://wg21.link/p349143
[P4340R0] Barry Revzin. 2026-08-13. Extending constant template parameter support by customizing std::meta::reflect_constant.
https://wg21.link/p4340r0