Reputation: 5194
I'm running into a strange behavior with the new spaceship operator <=>
in C++20. I'm using Visual Studio 2019 compiler with /std:c++latest
.
This code compiles fine, as expected:
#include <compare>
struct X
{
int Dummy = 0;
auto operator<=>(const X&) const = default; // Default implementation
};
int main()
{
X a, b;
a == b; // OK!
return 0;
}
However, if I change X to this:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
};
I get the following compiler error:
error C2676: binary '==': 'X' does not define this operator or a conversion to a type acceptable to the predefined operator
I tried this on clang as well, and I get similar behavior.
I would appreciate some explanation on why the default implementation generates operator==
correctly, but the custom one doesn't.
Upvotes: 87
Views: 7336
Reputation: 170064
This is by design.
[class.compare.default] (emphasis mine)
4 If the member-specification does not explicitly declare any member or friend named
operator==
, an==
operator function is declared implicitly for each three-way comparison operator function defined as defaulted in the member-specification, with the same access and function-definition and in the same class scope as the respective three-way comparison operator function, except that the return type is replaced withbool
and the declarator-id is replaced withoperator==
.
Only a defaulted <=>
allows a synthesized ==
to exist. The rationale is that classes like std::vector
should not use a non-defaulted <=>
for equality tests. Using <=>
for ==
is not the most efficient way to compare vectors. <=>
must give the exact ordering, whereas ==
may bail early by comparing sizes first.
If a class does something special in its three-way comparison, it will likely need to do something special in its ==
. Thus, instead of generating a potentially non-sensible default, the language leaves it up to the programmer.
Upvotes: 80
Reputation: 473352
During the standardization of this feature, it was decided that equality and ordering should logically be separated. As such, uses of equality testing (==
and !=
) will never invoke operator<=>
. However, it was still seen as useful to be able to default both of them with a single declaration. So if you default operator<=>
, it was decided that you also meant to default operator==
(unless you define it later or had defined it earlier).
As to why this decision was made (as shown stated in P1185), the basic reasoning goes like this. Consider std::string
. Ordering of two strings is lexicographical; each character has its integer value compared against each character in the other string. The first inequality results in the result of ordering.
However, equality testing of strings has a short-circuit. If the two strings aren't of equal length, then there's no point in doing character-wise comparison at all; they aren't equal. So if someone is doing equality testing, you don't want to do it long-form if you can short-circuit it.
It turns out that many types that need a user-defined ordering will also offer some short-circuit mechanism for equality testing. To prevent people from implementing only operator<=>
and throwing away potential performance, we effectively force everyone to do both.
Upvotes: 68
Reputation: 14714
The other answers explain really well why the language is like this. I just wanted to add that in case it's not obvious, it is of course possible to have a user-provided operator<=>
with a defaulted operator==
. You just need to explicitly write the defaulted operator==
:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
bool operator==(const X& other) const = default;
};
Note that the defaulted operator==
performs memberwise ==
comparisons. That is to say, it is not implemented in terms of the user-provided operator<=>
. So requiring the programmer to explicitly ask for this is a minor safety feature to help prevent surprises.
Upvotes: 31