We should strive to remove or avoid the following categories of inconsistencies:
x == y
differs
from the compile-time result of std::is_same_v<A<x>, A<y>>
for a suitably chosen template A
.x
as a top-level template
argument differs from its behavior as a member of a class, a value
of which is used as such a template argument.union U { int x; int y; }; template<int U::*p> struct A;
&U::x
and &U::y
(obviously) denote
different members of U
. According to
[temp.type] p1.3,
Two template-ids refer to the same class [...] if [...] their corresponding non-type template-arguments of pointer-to-member type refer to the same class member [...]
A<&U::x>
and A<&U::y>
are
therefore different types.
However, [expr.eq] p4.5 says about run-time comparison with ==
:
If both refer to (possibly different) members of the same union (11.4), they compare equal.Thus,
&U::x == &U::y
is true
.
This is a case of both a type (A) and a type (B) inconsistency,
because a pointer-to-member used as a class data member in a template
argument will be compared with ==
, but as a top-level
template argument, it will use the special "same class member" rule
quoted above.
<=>
comparison yields
std::partial_ordering
(see [expr.spaceship] p4.3).
With P1714R1 (NTTP are incomplete without float, double, and long double!) applied, values of floating-point type would have been permissible as a top-level template argument, but not as a class member. Thus, a type (B) inconsistency would exist because the use as a class member is ill-formed. Further, a type (A) inconsistency would be introduced. For example:
template<float x> struct A; static_assert<!std::is_same_v<A<+0.0>, A<-0.0>> // holds per P1714R1; different value representations constexpr bool b = +0.0 == -0.0; // true according to IEEE floating-point
Two template-ids refer to the same class [...] if [...] their corresponding non-type template-arguments of reference type refer to the same object or function [...]According to [class.compare.default] p2, the defaulted
operator==
is defined as delete if a class
contains a reference data member:
A defaulted comparison operator function for class C is defined as deleted if any non-static data member of C is of reference type or C is a union-like class (11.4.1).Thus, such a class is unusable as a non-type template parameter for lack of strong structural equality, introducing a type (B) inconsistency.
A type (A) inconsistency is present, because x ==
y
obviously does not consider whether x
and y
refer to the same object, but simply applies the
lvalue-to-rvalue conversion, erasing any notion of object identity.
operator==
operator==
may exhibit a type
(A) inconsistency, depending on the definition
of operator==
. Both class types and enum types may have a
user-defined operator==
. In the status quo, an absent or
non-defaulted operator==
for a class type prevents the
use of that class as the type of a non-type template parameter. There
is currently no comparable rule for enums, which leads to some
underspecification
(see P1837R0
Remove NTTPs of class type from C++20 by Arthur O'Dwyer).
operator==
operator==
, we could apply
the special equality rules in [temp.type] p1 also to class members of
the appropriate type. Ancillary restrictions, such as restricting the
permissible pointer values for non-type template arguments of pointer
type, already apply uniformly to class members as well as top-level
arguments; see [temp.arg.nontype] p2.
Whether a class is suitable as the type of a non-type template parameter in the first place, however, still needs to be determined. The following are possible options:
operator==
: After all,
if a user defines a non-defaulted operator==
somewhere,
it is a clear sign the class has special equality semantics, likely
causing even more surprising type (A) inconsistencies. For the
specific case of enums, which cannot have a
defaulted operator==
in their definition (because the
syntax doesn't allow it), a declaration of operator==
after the first use of that enum in a non-type template parameter
is considered hostile.
operator==
is not involved
in template argument equality anymore, there is no reason to refer
to it in any way. Unfathomable instantiations are prevented by the
rules on permissible template arguments; see [temp.arg.nontype] p2.memcpy
. However, trivial
copyability might not be related to comparison semantics.Regardless of the restriction chosen, the approach avoids any type (B) inconsistencies, but introduces more type (A) inconsistencies. However, type (A) inconsistencies appear to be historically acceptable, given the long-standing existing rules on pointer-to-members and references.