operator template()operator template()| Document #: | P4340R1 Latest Status |
| Date: | 2026-11-10 |
| Project: | Programming Language C++ |
| Audience: |
EWG |
| Reply-to: |
Barry Revzin <barry.revzin@gmail.com> |
Since [P4340R0], reworked the
customization point to
operator template
and added wording.
operator template
is now implicitly declared for all C++20 structural types, it cannot be
named, and idempotence is diagnosed.
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:
std::define_static_meow
functions in [P3491R3]
(define_static_{string,object,array})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.
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:
Widget author based on how they
define the object being returned (in this case, by comparing the
ints).Widget::operator template
(each template argument is the next element we’re serializing) and we
deserialize in the initialization of that variable template (where
likewise each template argument is the next element we’re
deserializing)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.
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:
Fraction type that is only in
lowest termsSmallString container that
doesn’t care about trailing data.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; } }
operator templateWhile 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.
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:
O has static storage
duration,O is usable in constant
expressions,O has type
T const
(this also guards against inheriting a customization
— Base’s implementation would have
to return a
Base const
object, which would be incompatible with the rules for a
Derived).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:
constevalT or
T const&.Note that a type with
mutable
members is non-structural, regardless of any customization point.
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):
std::optional<T>std::expected<T, E>std::variant<Ts...>std::tuple<Ts...>std::basic_string_view<charT, Traits>
(all specializations)std::span<T, Extent>
(all specializations)std::reference_wrapper<T>
(all specializations)std::complex<T>
(all specializations)<chrono>
types:
duration<Rep, Period>time_point<Clock, Duration>daymonthyearweekdayweekday_indexedweekday_lastmonth_daymonth_day_lastmonth_weekdaymonth_weekday_lastyear_monthyear_month_dayyear_month_day_lastyear_month_weekdayyear_month_weekday_laststd::partial_ordering,
std::weak_ordering,
std::strong_orderingstd::bitset<N>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.
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.
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…
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:
char const*
that the string_view owns, because
it’s a string literal, needs to be transformed through the string
literal process described abovestring_view needs to
go through its (defaulted)
operator template
opt-inThis 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.
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:
std::meta::reflect_constant.
Using that unqualified (whether by relying on ADL, a using directive, or
simply being in namespace
std::meta)
would now find the customization point we’re implementing, not the real
function. I did run into this bug.std::meta::reflect_constant
per se, so the premise wasn’t quite right.private (an
earlier idea required public), but even better would be to call this a
special member function. Since it really is quite
special, and I don’t think we want to have a special member function
called reflect_constantreflect_constant, you have to deal
with using
x.reflect_constant()
(not useful, since
std::meta::reflect_constant(x)
would do the same thing and work in more contexts) and
using Base::reflect_constant;
(which is wrong).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.
This has been implemented in my fork of Clang (updated for R1 in this commit), and you can see it on compiler explorer.
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.
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,
orconversion function, or template-argument function ([class.template.arg]), at least onedefining-type-specifierthat is not acv-qualifiershall appear in a completetype-specifier-seqor a completedecl-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]),
andprospective 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,
andhow 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 andref-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
F1of classCdiffers from the corresponding special member functionF2that would have been implicitly declared other than as follows:
- (2.1) if
F1is an assignment operator, it may have the&ref-qualifier;- (2.2) […]
- (2.3)
F1andF2may have differing exception specifications;- (2.4) […];
and- (2.5) if
F2has a non-object parameter of type “lvalue reference toC”, the corresponding non-object parameter ofF1may be of type “lvalue reference toconst C”; in this case only,F1is defined as deleted if it is explicitly defaulted on its first declaration and the program is ill-formed otherwise.; and- (2.6) if
F2is a template-argument function,F1may have an explicit object parameter of typeCor of type “lvalue reference toconst C”, in which case the type ofF1would differ from the type ofF2in that the type ofF1has 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-idis anoperator-function-idshall 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 () [] -> ->* ~ ! + - * / % ^ & | = += -= *= /= %= ^= &= |= == != < > <= >= <=> && || << >> <<= >>= ++ -- , template6 The
co_awaitoperator 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
Cis a non-static member function ofCwhose name is theoperator-function-idoperator templateand whose object parameter has typeconst C&orC, other than a function template or a specialization of one. The declarator of a template-argument function shall consist only of itsdeclarator-idfollowed by itsparameters-and-qualifiers. Theoperator-function-idoperator templateshall be used only in thedeclarator-idof a declaration of a template-argument function. [ Note 1: For example, it cannot be used in an expression, as the operand of areflect-expression, in ausing-declarator, or in atemplate-id. — end note ]2 The declared return type of a template-argument function is
std::meta::info. Nodecl-specifierin itsdecl-specifier-seqshall be adefining-type-specifier. A user-declared template-argument function shall be declaredconstevaland 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 asplice-expression, butstd::meta::members_ofcan 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
Cis an inline public member ofCand has the formconsteval 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
Cbe a class whose eligible template-argument functionFis user-provided. When the template parameter object for an objectxof typecv Cis determined ([temp.arg.nontype]),Fis invoked withxas its object argument, without regard to access control; letRbe the result of that invocation. If the template parameter object is determined as part of an evaluationXduring the evaluation of an expression as a core constant expression (for example, by a call tostd::meta::reflect_constant), the invocation is evaluated as part ofX. 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 unlessRrepresents either an object or a variable; letObe that object or the object declared by that variable. The program is ill-formed unless
- (7.1)
Ois the object declared by a variable with static storage duration, or a subobject of such an object,- (7.2)
Ois usable in constant expressions,- (7.3)
Ois a const object ([basic.type.qualifier]) of typeCorconst C,- (7.4) the complete object of
O([intro.object]) is not TU-local ([basic.link]) unlessxis TU-local, and- (7.5) the invocation of
FwithOas its object argument is a constant expression whose result represents eitherOor a variable that declaresO.
Ois the template parameter object ([temp.param]) forx.8 For any two objects
xandyof a class typeCwhose eligible template-argument function is user-provided, if the corresponding direct subobjects and reference members ofxandyare template-argument-equivalent ([temp.type]), the template parameter object forxshall be the same object as the template parameter object fory; 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
Cis defined as deleted if any direct base class ofC, any non-static data member ofCother than an anonymous union member, or any variant member ofChas a type that is neither a structural type ([temp.param]) nor a (possibly multidimensional) array of structural types.[ Example 1:— end example ]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
Change the definition of structural type in 13.2 Template parameters [temp.param]/12:
12 A structural type is one of the following:
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
distinctobjects 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 twosuchtemplate parameter objects have template-argument-equivalent values ([temp.type]). Anid-expressionnaming a constant template parameter of class typeTdenotes the template parameter object of typeconst 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
Pof (possibly deduced) typeTis determined from its template argumentAas follows. IfTis not a class type andAis not abraced-init-list,Ashall be a converted constant expression ([expr.const.const]) of typeT; the value ofPisthe normalized value (see below) ofA(as converted)A(as converted).3 Otherwise, a temporary variable
- constexpr T v = A; + constexpr T u = A;is introduced.
The lifetime ofvends immediately after initializing it and any template parameter object (see below). For each such variable, theid-expressionvis termed a candidate initializer.a If
Tis a class type whose eligible template-argument function is user-provided,Pdenotes the template parameter object ([class.template.arg]) foru. The lifetime ofuends immediately after that template parameter object is determined. [ Note: The template parameter object is not initialized fromu. — end note ]b Otherwise, a temporary variable
vof typeconst Tis introduced whose value is the normalized value (see below) ofu. The lifetimes ofuandvend immediately after initializingvand any template parameter object (see below). For each such variablev, theid-expressionvis termed a candidate initializer.
- (b.1) If
Tis a class type, a template parameter object ([temp.param]) exists that is constructed so as to be template-argument-equivalent tov;Pdenotes that template parameter object.Pis copy-initialized from an unspecified candidate initializer that is template-argument-equivalent tov. If, for the initialization from any candidate initializer,the program is ill-formed.
- (b.1.1) the initialization would be ill-formed, or
- (b.1.2) the full-expression of an invented
init-declaratorfor the initialization would not be a constant expression when interpreted as aconstant-expression([expr.const.const]), or- (b.1.3) the initialization would cause
Pto not be template-argument-equivalent ([temp.type]) tov,- (b.2) Otherwise, the value of
Pis that ofv.c For a string literal object
S([lex.string]), letPSbe the template parameter object of the same type asSwhose elements are template-argument-equivalent to the corresponding elements ofS. [ Note:PSis the object represented bystd::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
xof typeTis:
- (d.1) If
Tis a pointer type andxpoints to or past the end of an objectothat is a string literal objectSor a subobject ofS, the pointer value that points to or past the end of, respectively, the object that corresponds toowithinPS.- (d.2) Otherwise, if
Tis a reference type andxrefers to an objectothat is a string literal objectSor a subobject ofS, a reference to the object that corresponds toowithinPS.- (d.3) Otherwise, if
Tis an array type, the array value whose elements are the normalized values of the corresponding elements ofx.- (d.4) Otherwise, if
Tis a class type whose eligible template-argument function ([class.template.arg]) is user-provided, the value of an object of typeTcopy-initialized from an lvalue designating the template parameter object forx([class.template.arg]). The program is ill-formed if that copy-initialization would be ill-formed or if the full-expression of an inventedinit-declaratorfor it would not be a constant expression.- (d.5) Otherwise, if
Tis a union type,xifxhas no active member; otherwise, the value that has the same active member asx, whose value is the normalized value of the active member ofx.- (d.6) Otherwise, if
Tis a class type, the value whose direct subobjects and reference members have the normalized values of the corresponding direct subobjects and reference members ofx.- (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:— end 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
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):
- (6.1) a temporary object ([class.temporary]),
- (6.2)
a string literal object ([lex.string]),- (6.3) the result of a
typeidexpression ([expr.typeid]),- (6.4) a predefined
__func__variable ([dcl.fct.def.general]), or- (6.5) a subobject ([intro.object]) of one of the above.
Change the note in 13.4.3 Constant template arguments [temp.arg.nontype]/9:
[ Note: A
string-literal([lex.string]) isnotan acceptabletemplate-argumentfor a constant template parameter ofnon-classpointer or reference type; the resulting pointer value or reference is normalized to point into or refer to a template parameter object.[ Example:— end note ]— end 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
[ 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. ]
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 Correspondingoperator-function-id Operator symbol name[…] […] […] op_plus_plusoperator++++op_minus_minusoperator----op_commaoperator,,op_templateoperator templatetemplate
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 only7 Throws:
meta::exceptionunless
- (7.1)
rrepresents a non-static data member with typeX, that is not a bit-field, that is a direct member of classC,TandX C::*are similar types ([conv.qual]), andis_convertible_v<X C::*, T>istrue;- (7.2)
rrepresents an implicit object member function that is not a template-argument function ([class.template.arg]) with typeForF noexceptthat is a direct member of a classC, andTisF C::*; or- (7.3)
rrepresents a non-member function, static member function, or explicit object member function that is not a template-argument function of function typeForF noexcept, andTisF*.
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
vectorcontaining reflections of the template arguments of the template specialization represented byr, in the order in which they appear in the corresponding template argument list. For a given template argumentA, its corresponding reflectionRis determined as follows:
- (59.1) If
Adenotes a type or type alias, […]- (59.2) Otherwise, if
Adenotes a class template, variable template, concept, or alias template, […]- (59.3) Otherwise,
Ais a constant template argument ([temp.arg.nontype]). LetPbe the corresponding template parameter.
- (59.3.1) If
Phas reference type, thenRis a reflection representing the object or function referred to byAPas determined fromA([temp.arg.nontype]).- (59.3.2) Otherwise, if
Phas class type, thenRrepresents the corresponding template parameter object.- (59.3.3) Otherwise,
Ris a reflection representing the value ofAPas determined fromA([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>istrueandTis a cv-unqualified structural type ([temp.param]) that is not a reference type.2 Let
Vbe:
- (2.1) if
Tis a class type, then an object that is template-argument-equivalent to the value ofexpr;- (2.2) otherwise, the value of
expr.3 Let
TClsbe 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::exceptionunless thetemplate-idTCls<V>would be valid.[ Example:— end 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]);
[ 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).
optionalAdd a paragraph to the end of 22.5.3.1 General [optional.optional.general]:
3
optional<T>is a structural type ([term.structural.type]) ifTis a structural type. Two valuesxandyof typeoptional<T>are template-argument-equivalent ([temp.type]) if and only if eitherx.has_value()andy.has_value()are bothfalse, or they are bothtrueandx.valandy.valare 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 valuesxandyof typeoptional<T&>are template-argument-equivalent ([temp.type]) if and only if eitherx.has_value()andy.has_value()are bothfalse, or they are bothtrueand*xand*yrefer to the same object.
variantAdd 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 inTypesis a structural type. Two valuesxandyof typevariant<Types...>are template-argument-equivalent ([temp.type]) if and only ifx.index() == y.index()istrueand eitherx.valueless_by_exception()istrueor the objects referred to byget<I>(x)andget<I>(y)are template-argument-equivalent, whereIisx.index().
tupleAdd 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 inTypesis a structural type. Two valuesxandyof typetuple<Types...>are template-argument-equivalent ([temp.type]) if and only if, for everyIin the range [0,sizeof...(Types)):
expectedAdd 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]) ifTandEare both structural types. Two valuesxandyof typeexpected<T, E>are template-argument-equivalent ([temp.type]) if and only if eitherx.has_value()andy.has_value()are bothtrueandx.valandy.valare template-argument-equivalent, or they are bothfalseandx.unexandy.unexare 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]) ifEis a structural type. Two valuesxandyof typeexpected<T, E>are template-argument-equivalent ([temp.type]) if and only if eitherx.has_value()andy.has_value()are bothtrue, or they are bothfalseandx.unexandy.unexare template-argument-equivalent.
reference_wrapperAdd 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 valuesxandyof typereference_wrapper<T>are template-argument-equivalent ([temp.type]) if and only ifx.get()andy.get()refer to the same object or function.
Add a paragraph to the end of 17.12.2.1 Preamble [cmp.categories.pre]:
5 The types
partial_ordering,weak_ordering, andstrong_orderingare structural types ([term.structural.type]). Two valuesxandyof the same such type are template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
bitsetAdd 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 valuesxandyof typebitset<N>are template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
basic_string_viewAdd 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 valuesxandyof typebasic_string_view<charT, traits>are template-argument-equivalent ([temp.type]) if and only ifx.data()andy.data()are template-argument-equivalent andx.size() == y.size()istrue.
spanAdd 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 valuesxandyof typespan<ElementType, Extent>are template-argument-equivalent ([temp.type]) if and only ifx.data()andy.data()are template-argument-equivalent andx.size() == y.size()istrue.
inplace_vectorFor 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:
size()
consists of objects that are not within their lifetime, so that
template-argument-equivalence ignores them, or0,
size()),
like SmallString and
vector in the design
discussion.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]) ifTis a structural type. Two constant template arguments of typeinplace_vector<T, N>whose values arexandydetermine the same template parameter object ([temp.arg.nontype]) if and only ifx.size() == y.size()istrueand the objects referred to byx[i]andy[i]are template-argument-equivalent for eachiin the range [0,x.size()).
complexAdd a paragraph to the end of 29.4.3 Class template complex [complex]:
2
complex<T>is a structural type ([term.structural.type]). Two valuesxandyof typecomplex<T>are template-argument-equivalent ([temp.type]) if and only ifx.real()andy.real()are template-argument-equivalent andx.imag()andy.imag()are template-argument-equivalent.
<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]) ifRepis a structural type. Two valuesxandyof typeduration<Rep, Period>are template-argument-equivalent ([temp.type]) if and only ifx.count()andy.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]) ifDurationis a structural type. Two valuesxandyof typetime_point<Clock, Duration>are template-argument-equivalent ([temp.type]) if and only ifx.time_since_epoch()andy.time_since_epoch()are template-argument-equivalent.
Add a paragraph to the end of 30.8.3.1 Overview [time.cal.day.overview]:
3
dayis a structural type ([term.structural.type]). Two valuesxandyof typedayare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.4.1 Overview [time.cal.month.overview]:
3
monthis a structural type ([term.structural.type]). Two valuesxandyof typemonthare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.5.1 Overview [time.cal.year.overview]:
3
yearis a structural type ([term.structural.type]). Two valuesxandyof typeyearare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.6.1 Overview [time.cal.wd.overview]:
3
weekdayis a structural type ([term.structural.type]). Two valuesxandyof typeweekdayare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.7.1 Overview [time.cal.wdidx.overview]:
4
weekday_indexedis a structural type ([term.structural.type]). Two valuesxandyof typeweekday_indexedare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.8.1 Overview [time.cal.wdlast.overview]:
4
weekday_lastis a structural type ([term.structural.type]). Two valuesxandyof typeweekday_lastare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.9.1 Overview [time.cal.md.overview]:
3
month_dayis a structural type ([term.structural.type]). Two valuesxandyof typemonth_dayare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph after paragraph 3 of 30.8.10 Class month_day_last [time.cal.mdlast]:
a
month_day_lastis a structural type ([term.structural.type]). Two valuesxandyof typemonth_day_lastare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.11.1 Overview [time.cal.mwd.overview]:
4
month_weekdayis a structural type ([term.structural.type]). Two valuesxandyof typemonth_weekdayare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.12.1 Overview [time.cal.mwdlast.overview]:
4
month_weekday_lastis a structural type ([term.structural.type]). Two valuesxandyof typemonth_weekday_lastare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.13.1 Overview [time.cal.ym.overview]:
3
year_monthis a structural type ([term.structural.type]). Two valuesxandyof typeyear_monthare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.14.1 Overview [time.cal.ymd.overview]:
3
year_month_dayis a structural type ([term.structural.type]). Two valuesxandyof typeyear_month_dayare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.15.1 Overview [time.cal.ymdlast.overview]:
3
year_month_day_lastis a structural type ([term.structural.type]). Two valuesxandyof typeyear_month_day_lastare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.16.1 Overview [time.cal.ymwd.overview]:
3
year_month_weekdayis a structural type ([term.structural.type]). Two valuesxandyof typeyear_month_weekdayare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
Add a paragraph to the end of 30.8.17.1 Overview [time.cal.ymwdlast.overview]:
3
year_month_weekday_lastis a structural type ([term.structural.type]). Two valuesxandyof typeyear_month_weekday_lastare template-argument-equivalent ([temp.type]) if and only ifx == yistrue.
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>
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.
define_static_{string,object,array}.