non-defaulted operator <=> doesn't generate == and != in C++20
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.
This is by design.
[class.compare.default] (emphasis mine)
3 If the class definition does not explicitly declare an
==
operator function, but declares a defaulted three-way comparison operator function, an==
operator function is declared implicitly with the same access as the three-way comparison operator function. The implicitly-declared==
operator for a class X is an inline member and is defined as defaulted in the definition of X.
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.
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, 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.
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.