Non-defaulted operator <=> doesn't generate == and !=
Asked Answered
C

3

80

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.

Churchwarden answered 9/11, 2019 at 15:43 Comment(3)
The title makes it harder to reach this question when googling. Maybe should change to non-defaulted operator <=> doesn't generate == and !=. I happened to encounter the motivation behind p1185r2 and was going to ask a similar question and answer it myself.Nightspot
I would like to keep this question open, because I find the answer containing the Specs quote much better then the answers to the non-dup'd question.Indus
I came here from stackoverflow.com/questions/77981799/…. It's a shame compilers don't have a nice explicit error for this. It wouldn't be hard and it would be very helpful: ...did you mean to add bool operator==(const X&) const = default;?Timmerman
F
75

This is by design.

[class.compare.default] (emphasis mine)

4 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.

Flagrant answered 9/11, 2019 at 15:56 Comment(2)
It's certainly sensible, unless spaceship is buggy. Potentially grossly inefficient though...Indue
@Indue - Sensibility is subjective. Some would say a silently generated inefficient implementation to be not sensible.Flagrant
R
63

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.

Rawlings answered 9/11, 2019 at 15:54 Comment(0)
J
28

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.

Jovitajovitah answered 10/11, 2019 at 21:5 Comment(1)
And maybe note that the defaulted operator== is not using operator<=>.Piraeus

© 2022 - 2024 — McMap. All rights reserved.