Why does operator== only get generated for *defaulted* operator<=>?

To elaborate on the title, consider a defaulted three-way comparison operator for a type:

``` struct MyType { auto operator<=>(const MyType& other) const = default; int value; };

int main() { MyType a{ 42 }, b{ 24 }; return a == b; // works fine } ```

Now if we change that three-way comparison operator to no longer be defaulted, but still have the same effect:

``` struct MyType { auto operator<=>(const MyType& other) const { return value <=> other.value; } int value; };

int main() { MyType a{ 42 }, b{ 24 }; return a == b; // no longer works } ```

Godbolt: https://godbolt.org/z/uyqRo6

It is easy (but annoying) to work around by defining bool operator==(const MyType& other) const { return std::is_eq(*this <=> other); }, but I feel like I shouldn't have to do that.

Is there some logical reason why the equality operators can never be generated from a non-defaulted three-way comparison operator?

I'm already aware of the potential performance losses with three-way comparisons on complex types, but for simple struct types I feel like this should be automatic and just as performant.

18 comments · 83% upvoted · other discussions

comments

tvaneerd

Because building == from <=> can result in a less performant == than building == directly. (Yet building <, <=, etc from <=> is not typically slower.)

For example, a performant == on a container would first check whether the sizes were equal, before checking the elements. But <=> needs to check the elements to determine which is less.

In general, <=> returns more information than ==, so it may need to do more work.

The real question is why does <=> generate == when defaulted, instead of keeping them completely separate always.

rasm866i

I suppose the point is that when you give the compiler the freedom to generate the lexicographical compare, it also knows how to efficiently generate the equality operators, and there it no reason to not let it do that. If you define the semantics of <=> to be lexicographical compare, then it SHOULD be a hassle to get different semantics for == and !=. This is different when you write it yourself, because then you define non-standard semantics on comparisons, which the compiler has no idea how to translate efficiently to equality.

​

I do have a question here tho: If a MyType defined above had a user type with a userdefined <=> but no ==, would defaulting <=> on MyType generate == and !=, even though it does not know how to do that efficiently?

tvaneerd

hmmm, don't know what it would do in that case (someone should try it in godbolt). Either it doesn't compile, or it generates <=> without generating ==. But MyType would be a bad type anyhow, so I'd be OK with whatever happens.

rasm866i

https://godbolt.org/z/Fo-JTy

Turns out, it does the sensible thing:

doesn't try to auto-generate possibly sub-optimal == and != when those are not available from the member types.

Who would have guessed that the c++ committee makes sensible choises? :D

Pazer2 [OP]

Why would MyType be a bad type?

guyonahorse

You can always add in an explicit default for operator==

auto operator==(const MyType& other) const = default;

Edit: It doesn't do this automatically because specifying a custom operator<=> disables auto generating operator== and forces you to explicitly say how to do it (either the default way or your own way).

(wrong)It doesn't do this automatically because you can nest comparable types, and if someone specializes the deepest <=> you want all of the outer ones to break unless they explicitly opt-back in to the default one.(/wrong)

Pazer2 [OP]

The problem is, I can't just default the operator== because there is some custom comparison logic in operator<=>. Otherwise I would just default operator<=> and I wouldn't have this problem in the first place.

I don't think I'm understanding your second paragraph. Why would you want all the outer ones to break? I can understand if the result type changed, but in this case it's still the strongest one (std::strong_ordering).

EDIT: Are you saying that defaulting operator== when operator<=> is specialized causes it to generate an implementation of operator== in terms of operator<=>? Seems unintuitive to me, I would assume a defaulted comparison operator would always just be generated based on the class/struct members, same as defaulted operator<=> is.

guyonahorse

Here's the full story of why == and <=> were separated in this case:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1185r2.html#motivation

guyonahorse

Replying to your edit.. a defaulted operator== doesn't use the operator<=> it just does operator== on all member types as it normally would by default. I was over thinking your example that did the default behavior for operator<=> and thought you just wanted default behavior for operator== (not to use your operator<=> for operator==).

guyonahorse

Ignore my second part.. I tried it and it doesn't do that (the operator<=> spec changed a few times and I can't remember if it actually did that or I just misremembered).

I updated my comment with the correct version. Sorry for the confusion there.

CoffeeTableEspresso

This defaulted version falls back on <=>, right?

I'm not completely up to date on all the C++20 stuff yet..

guyonahorse

No, the default does operator== on all of the contained types (I double checked). It's confusing since it has changed a few times (see my reply above on the history of why)

CoffeeTableEspresso

Thanks! I must have read an older version, it's so hard to keep up with C++...

xcbsmith

Much as with the STL, simply providing the logically correct answer is not enough. There are expectations around efficiency/complexity for comparison operators that <=> can't really provide. So, on a principle of least surprise, you fail loudly rather than fail silently.

wheypoint

on a principle of least surprise "= default"ed operator <=> should never give you operator==.

You should always have to =default it explicitly

dscharrer

IMO defaulted operator== should be the default, just like operator= and you should have to explicitly delete it if you don't want comparison (or have a member with it deleted). But that would be a breaking change so enabling operator== with operator<=> = default adds back some of the convenience lost due to backwards compatibility.

I don't find it that surprising that if you tell the compiler to just sort this class the straightforward way that that also implies to compare it for equality in a similar straight forward way as mathematically equality can be derived from order (even if in C++ the concepts are separate for efficiency reasons).

wheypoint

IMO defaulted operator== should be the default

definetely agree with that, might be possible to add w epochs (should those ever exist)

my reply was to 'on a principle of least surprise':

operator<=> = default implying operator== = default isnt per se wrong, but i think it is harder to teach because it is now even harder to draw the line between comparisons ==/<=>

MonokelPinguin

I think the surprise is reasonable in this case though. If you manually define <=>, it's good that you are surprised about a missing ==, since you will probably find a better operator== to implement. Surprise can be a good teaching tool and in all the efficient cases you still get the ergonomic behaviour.