It seems that the real question that can be answered here is about the history of this feature, so that whatever compiler support can be understood in context.
Limitations on non-type template parameter types
People have been wanting class-type non-type template parameters for a long time. The answers there are somewhat lacking; what really makes support for such template parameters (really, of non-trivial user-defined types) complicated is their unknown notion of identity: given
struct A {/*...*/};
template<A> struct X {};
constexpr A f() {/*...*/}
constexpr A g() {/*...*/}
X<f()> xf;
X<g()> &xg=xf; // OK?
how do we decide whether X<f()>
and X<g()>
are the same type? For integers, the answer seems intuitively obvious, but a class type might be something like std::vector<int>
, in which case we might have
// C++23, if that
using A=std::vector<int>;
constexpr A f() {return {1,2,3};}
constexpr A g() {
A ret={1,2,3};
ret.reserve(1000);
return ret;
}
and it's not clear what to make of the fact that both objects contain the same values (and hence compare equal with ==
) despite having very different behavior (e.g., for iterator invalidation).
P0732 Class types in non-type template parameters
It's true that this paper first added support for class-type non-type template parameters, in terms of the new <=>
operator. The logic was that classes that defaulted that operator were "transparent to comparisons" (the term used was "strong structural equality") and so programmers and compilers could agree on a definition of identity.
Later it was realized that ==
should be separately defaultable for performance reasons (e.g., it allows an early exit for comparing strings of different lengths), and the definition of strong structural equality was rewritten in terms of that operator (which comes for free along with a defaulted <=>
). This doesn't affect this story, but the trail is incomplete without it.
P1714 NTTP are incomplete without float, double, and long double!
It was discovered that class-type NTTPs and the unrelated feature of constexpr std::bit_cast
allowed a floating-point value to be smuggled into a template argument inside a type like std::array<std::byte,sizeof(float)>
. The semantics that would result from such a trick would be that every representation of a float
would be a different template argument, despite the fact that -0.0==0.0
and (given float nan=std::numeric_limits<float>::quiet_NaN();
) nan!=nan
. It was therefore proposed that floating-point values be allowed directly as template arguments, with those semantics, to avoid encouraging widespread adoption of such a hacky workaround.
At the time, there was a lot of confusion around the idea that (given template<auto> int vt;
) x==y
might differ from &vt<x>==&vt<y>
), and the proposal was rejected as needing more analysis than could be afforded for C++20.
P1907R0 Inconsistencies with non-type template parameters
It turns out that ==
has a lot of problems in this area. Even enumerations (which have always been allowed as template parameter types) can overload ==
, and using them as template arguments simply ignores that overload entirely. (This is more or less necessary: such an operator might be defined in some translation units and not others, or might be defined differently, or have internal linkage, etc.) Moreover, what an implementation needs to do with a template argument is canonicalize it: to compare one template argument (in, say, a call) to another (in, say, an explicit specialization) would require that the latter had somehow already been identified in terms of the former while somehow allowing the possibility that they might differ.
This notion of identity already differs from ==
for other types as well. Even P0732 recognized that references (which can also be the type of template parameters) aren't compared with ==
, since of course x==y
does not imply that &x==&y
. Less widely appreciated was that pointers-to-members also violate this correspondence: because of their different behavior in constant evaluation, pointers to different members of a union are distinct as template arguments despite comparing ==
, and pointers-to-members that have been cast to point into a base class have similar behavior (although their comparison is unspecified and hence disallowed as a direct component of constant evaluation).
In fact, in November 2019 GCC had already implemented basic support for class-type NTTPs without requiring any comparison operator.
P1837 Remove NTTPs of class type from C++20
These incongruities were so numerous that it had already been proposed that the entire feature be postponed until C++23. In the face of so many problems in so popular a feature, a small group was commissioned to specify the significant changes necessary to save it.
P1907R1 (structural types)
These stories about template arguments of class type and of floating-point type reconverge in the revision of P1907R0 which retained its name but replaced its body with a solution to National Body comments that had also been filed on the same subject. The (new) idea was to recognize that comparisons had never really been germane, and that the only consistent model for template argument identity was that two arguments were different if there was any means of distinguishing them during constant evaluation (which has the aforementioned power to distinguish pointers-to-members, etc.). After all, if two template arguments produce the same specialization, that specialization must have one behavior, and it must be the same as would be obtained from using either of the arguments directly.
While it would be desirable to support a wide range of class types, the only ones that could be reliably supported by what was a new feature introduced (or rather rewritten) at almost the last possible moment for C++20 were those where every value that could be distinguished by the implementation could be distinguished by its clients—hence, only those that have all public members (that recursively have this property). The restrictions on such structural types are not quite as strong as those on an aggregate, since any construction process is permissible so long as it is constexpr. It also has plausible extensions for future language versions to support more class types, perhaps even std::vector<T>
—again, by canonicalization (or serialization) rather than by comparison (which cannot support such extensions).
The general solution
This newfound understanding has no relationship to anything else in C++20; class-type NTTPs using this model could have been part of C++11 (which introduced constant expressions of class type). Support was immediately extended to unions, but the logic is not limited to classes at all; it also established that the longstanding prohibitions of template arguments that were pointers to subobjects or that had floating-point type had also been motivated by confusion about ==
and were unnecessary. (While this doesn't allow string literals to be template arguments for technical reasons, it does allow const char*
template arguments that point to the first character of static character arrays.)
In other words, the forces that motivated P1714 were finally recognized as inevitable mathematical consequences of the fundamental behavior of templates and floating-point template arguments became part of C++20 after all. However, neither floating-point nor class-type NTTPs were actually specified for C++20 by their original proposals, complicating "compiler support" documentation.