57
#include <compare>

struct A
{
    int n;

    auto operator<=>(A const& other) const
    {
        if (n < other.n)
        {
            return std::strong_ordering::less;
        }
        else if (n > other.n)
        {
            return std::strong_ordering::greater;
        }
        else
        {
            return std::strong_ordering::equal;
        }
    }

    // compile error if the following code is commented out.
    // bool operator==(A const& other) const
    // { return n == other.n; }
};

int main()
{   
    A{} == A{};
}

See online demo

Why must I provide operator == when operator <=> is enough?

cigien
  • 57,834
  • 11
  • 73
  • 112
xmllmx
  • 39,765
  • 26
  • 162
  • 323
  • 1
    Why does `<=>` not include `==`? I mean, if `==` is provided, use it; if not, use `<=>` instead? Why does the C++ standard not be designed in this way? – xmllmx Jul 02 '21 at 07:18
  • 5
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1190r0.html – jamesdlin Jul 02 '21 at 07:38
  • @jamesdlin - Looking at the paper - the obvious solution for me would be to combine a new return type with pascal-lexicographic ordering. (So basically opt-in to say that <=> is efficient for == and thus default-generate, by using a special return type for <=> - and generate efficient <=> using pascallexicographic ordering). Was this discussed? – Hans Olsson Jul 02 '21 at 08:07
  • 6
    Ya'know... that second duplicate I linked is also asked by you.... – StoryTeller - Unslander Monica Jul 02 '21 at 08:26
  • @HansOlsson: That assumes that there's ever a time when you provide a user-defined `<=>` where a hand-rolled `==` would be no faster than the user-defined `<=>` for equality testing. Can you show me such a case, a case that *needs* user-defined ordering where user-defined equality cannot be done faster than the ordering test? And no, strings don't qualify: you can do size testing to short-circuit equality testing, which you can't do with ordering. – Nicol Bolas Jul 02 '21 at 13:24
  • @NicolBolas Yes, the linked paper discusses switching to something they call pascal-lexicographic ordering for vectors (and similar collections) that allows that short-circuit size-testing. Clearly that would be a different ordering, but in many applications that wouldn't matter as long as it is consistent and a user would be able to opt-in to that by using a different return-type for <=>. – Hans Olsson Jul 02 '21 at 14:36
  • 1
    @HansOlsson: You cannot change the meaning and behavior of peoples' code out from under them. Also, you can't overload on the basis of return types, so there's no way to *request* a specific kind of ordering. You can only use what the type provides, and the existing ordering operators for standard library types are already being relied upon. What you suggest is non-workable. – Nicol Bolas Jul 02 '21 at 14:49
  • @NicolasBolas And yet that was proposed to WG21, although it was described as overloading based on a comparison category, not on return type. And even if non-workable for std::vector it would work for a user-defined collection. I wouldn't be surprised if we see something like this within the next 10-20 years. – Hans Olsson Jul 02 '21 at 15:18
  • 3
    This question is a duplicate of this other question: https://stackoverflow.com/q/58780829/1896169 , but I don't want to close this as a duplicate because the answers here provide different information / different viewpoints to help understand the same information... – Justin Jul 02 '21 at 20:51
  • As a comment, Haskell defines a default `compare` (equivalent of C++'s `<=>`) using `==`, with the requirement of having to define `<=` first. It doesn't do it the other way around (defining `==` in terms of `compare`/`<=>`) so that things can have equality without them having to be comparable/`<=>`-able. For example, if you have a union type (enum) for currency, 2 currencies can be equal, but it doesn't make sense for one to be greater than the other. Conversely, if it makes sense for objects to be comparable as one being greater, then it also makes sense for them to be equal or not equal. – JoL Jul 02 '21 at 21:57
  • @Justin, at some point in the distant past, I seem to recall SO was intending to merge questions that were considered dupes, including the answers, which would fix your particular reticence to close. Not sure what happened to that scheme, it may be too hard to do automatically? – paxdiablo Jul 03 '21 at 01:11
  • @HansOlsson: "*And even if non-workable for std::vector it would work for a user-defined collection.*" How you define your comparison operator is up to you. There is nothing inherently wrong with doing it the way you suggest. But that is hardly the default; most types that exist which *need* user-defined conversions have faster equality testing. And if you want to do it that way, you can make a quick `operator==` overload that just defers to the `<=>`. Nobody's making you duplicate code. – Nicol Bolas Jul 03 '21 at 03:13
  • @paxdiablo AFAIK, moderators can merge questions with duplicates. But because I've been mostly hands-off in my recent activity on SO, I'm reluctant to participate in such edge cases.... – Justin Jul 03 '21 at 04:44
  • 1
    @Justin - I do think it's a duplicate. Not of the one you link, but of [1](https://stackoverflow.com/questions/61039897/why-can-i-invoke-with-a-defaulted-but-not-a-user-provided-one) and [2](https://stackoverflow.com/questions/61488372/why-default-three-way-operator-spaceship-generates-equality-operator). Unfortunately, I already voted, only to have this here reopened under shady optics. – StoryTeller - Unslander Monica Jul 03 '21 at 13:42
  • 1
    Does this answer your question? [Why can I invoke == with a defaulted <=> but not a user-provided one?](https://stackoverflow.com/questions/61039897/why-can-i-invoke-with-a-defaulted-but-not-a-user-provided-one) – OrangeDog Jul 04 '21 at 09:53
  • @StoryTeller, I'm not *sure* what you meant by shady optics but I'll assume for now it's that you simply disagree with the question being reopened (by me). That's fine, we're both entitled to our opinions, but I'm usually circumspect about ascribing motives based on limited knowledge. And assuming you're right when others are wrong is as bad as me doing the same :-) For more explanation, I did *see* the proposed dupe and decided, though borderline, that the question being asked here was just different enough to be considered distinct (cont). – paxdiablo Jul 05 '21 at 12:34
  • (cont) And disagreements happen, that's why the "swarm" nature of SO is so good - no one person has too much power and consensus requires buy-in from lots of different people. I notice from the history that this question went through the review queues after reopening and all three votes opted to leave it open, so I don't think I'm alone in my view. Anyway, didn't want to start an argument, just wanted to clarify the thought process I generally follow when deciding these things. I don't always get it right (especially borderline cases) but I think (or hope) I do better than average overall. – paxdiablo Jul 05 '21 at 12:35
  • 1
    @Justin admins can merge the answers, this should be closed so that all the answers can live together – OrangeDog Jul 05 '21 at 12:44
  • @paxdiablo - And yet, you prescribe motive yourself. Pointing out that the optics of an action are shady are not attribution of malice. Jumping on the defensive however, does seem more indicative than the reopening itself. My mark would be less-than-average however. As far as the 3 reviewers... nah, there's plenty of getting swayed by votes in the review queue. As well as reviews by non-subject matter experts. I wouldn't try to lean on it if I were you. Now if you are *truly* okay with mild disagreement, we can leave it at that. – StoryTeller - Unslander Monica Jul 05 '21 at 13:13
  • No problems, @storyteller, I read shady as suspicious or somehow wrong but I didn't mean to assume malice, which is why I stated it was most likely just mild disagreement between us. Happy to leave as is. If it gets merged, I'm fine with that too. Cheers. – paxdiablo Jul 05 '21 at 23:12

5 Answers5

62

Why must I provide operator== when operator<=> is enough?

Well, mainly because it's not enough :-)

Equality and ordering are different buckets when it comes time for C++ to rewrite your statements:

Equality Ordering
Primary == <=>
Secondary != <, >, <=, >=

Primary operators have the ability to be reversed, and secondary operators have the ability to be rewritten in terms of their corresponding primary operator:

  • reversing means that a == b can be either:
    • a.operator==(b) if available; or
    • b.operator==(a) if not.
  • rewriting means that a != b can be:
    • ! a.operator==(b) if available

That last one could also be ! b.operator==(a) if you have to rewrite and reverse it (I'm not entirely certain of that since my experience has mostly been with the same types being compared).

But the requirement that rewriting not take place by default across the equality/ordering boundary means that <=> is not a rewrite candidate for ==.


The reason why equality and ordering are separated like that can be found in this P1185 paper, from one of the many standards meetings that discussed this.

There are many scenarios where automatically implementing == in terms of <=> could be quite inefficient. String, vector, array, or any other collections come to mind. You probably don't want to use <=> to check the equality of the two strings:

  • "xxxxx(a billion other x's)"; and
  • "xxxxx(a billion other x's)_and_a_bit_more".

That's because <=> would have to process the entire strings to work out ordering and then check if the ordering was strong-equal.

But a simple length check upfront would tell you very quickly that they were unequal. This is the difference between O(n) time complexity, a billion or so comparisons, and O(1), a near-immediate result.


You can always default equality if you know it will be okay (or you're happy to live with any performance hit it may come with). But it was thought best not to have the compiler make that decision for you.

In more detail, consider the following complete program:

#include <iostream>
#include <compare>

class xyzzy {
public:
    xyzzy(int data) : n(data) { }

    auto operator<=>(xyzzy const &other) const {
        // Could probably just use: 'return n <=> other.n;'
        // but this is from the OPs actual code, so I didn't
        // want to change it too much (formatting only).

        if (n < other.n) return std::strong_ordering::less;
        if (n > other.n) return std::strong_ordering::greater;
        return std::strong_ordering::equal;
    }

    //auto operator==(xyzzy const &other) const {
    //    return n == other.n;
    //}

    //bool operator==(xyzzy const &) const = default;

private:
    int n;
};

int main() {
    xyzzy twisty(3);
    xyzzy passages(3);

    if (twisty < passages) std::cout << "less\n";
    if (twisty == passages) std::cout << "equal\n";
}

It won't compile as-is since it needs an operator== for the final statement. But you don't have to provide a real one (the first commented-out chunk), you can just tell it to use the default (the second). And, in this case, that's probably the correct decision as there's no real performance impact from using the default.


Keep in mind that you only need to provide an equality operator if you explicitly provide a three-way comparison operator (and you use == or !=, of course). If you provide neither, C++ will give you both defaults.

And, even though you have to provide two functions (with one possibly being a defaulted one), it's still better than previously, where you had to explicitly provide them all, something like:

  • a == b.
  • a < b.
  • a != b, defined as ! (a == b).
  • a > b, defined as ! (a < b || a == b).
  • a <= b, defined as a < b || a == b.
  • a >= b, defined as ! (a < b).
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • 16
    To Aryan, who edited my answer with the new table format, thanks greatly. I didn't even know that was an option (you would think someone who's been here over a decade *would* know something like that). My future answers are going to be greatly improved with this newfound knowledge. Well, at least in formatting, if not content :-) – paxdiablo Jul 03 '21 at 02:16
  • 5
    to be fair to you, the table format is [only 7-month-old](https://meta.stackexchange.com/questions/356997/new-feature-table-support) – justhalf Jul 04 '21 at 18:41
11

Why must I provide 'operator ==' when 'operator <=>' is enough?

Because it won't be used.

It will be enough if you were to use the defaulted one:

struct A
{
    int n;
    auto operator<=>(A const& other) const = default;
};

Basically, n == n is potentially more efficient than (a <=> a) == std::strong_ordering::equal and there are many cases where that is an option. When you provide a user defined <=>, the language implementation cannot know whether latter could be substituted with the former, nor can it know whether latter is unnecessarily inefficient.

So, if you need a custom three way comparison, then you need a custom equality comparison. The example class doesn't need a custom three way comparison, so you should use the default one.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • 1
    Wouldn't that be a reason for a warning only? – einpoklum Jul 02 '21 at 15:39
  • @einpoklum All warnings are at implementation's discretion. If there is no matching overloaded == nor defaulted <=>, then using == will result in the program being ill-formed. The language implementation must diagnose ill-formed programs, but it isn't required to be an error. If it isn't, that's typically considered to be a language extension. – eerorika Jul 02 '21 at 15:51
  • Then I don't see how it is justified to force a user to write the extra code just because otherwise is less efficient. C++ doesn't force users to write move constructors, for example, which often causes a lot of useless copying. – einpoklum Jul 02 '21 at 15:55
  • @einpoklum The compiler will implicitly generate a move constructor without a need to write one. – eerorika Jul 02 '21 at 15:58
  • In general, it won't. Under some [specific conditions](https://en.cppreference.com/w/cpp/language/move_constructor) it might. – einpoklum Jul 02 '21 at 17:02
  • @einpoklum I beg to disagree. The compiler generates a move constructor whenever it's safe to do so, I see no missed opportunities. The only problematic case when it doesn't do so (leading to silent performance degradation) is when you have a custom destructor, but the fact that copy operations are generated in that case is deprecated. – HolyBlackCat Jul 02 '21 at 17:14
  • Potato, potato... it's not safe in many cases. And then, the compiler has, say, std::vector copy objects with abandon when it resizes. Super-unoptimal. So why not equality via `<=>`? – einpoklum Jul 02 '21 at 18:02
  • 4
    @einpoklum: "potato, potato" sort of loses its effect when written, it just looks like the writer has gone insane :-) . Perhaps "potayto, potarto" would be better. – paxdiablo Jul 03 '21 at 02:06
7

Looking at the previous answers, nobody has addressed another issue: For ordering purposes, two objects might be equivalent and yet not be equal. For example, I might want to sort on strings in Unicode NFC normalization with case-folding to lowercase, but for equality testing, I want to verify that the strings are actually identical, with case being significant and perhaps even distinguishing between é and ´ + e in the input.

Yes, this is a somewhat contrived example, but it serves to make the point that the definition of <=> does not require strong ordering so you cannot rely on <=> even potentially returning std::strong_ordering::equal. Making == default to <=> returns std::strong_ordering::equal cannot be assumed to be a valid implementation.

Don Hosek
  • 981
  • 6
  • 23
  • 6
    I would consider a type broken if it did this. To me it would be a reasonable default implementation (if not for performance problems), since you could always override it. – HolyBlackCat Jul 02 '21 at 17:29
  • 2
    Yeah, I'm not sure I could *trust* a type that used different rules for ordering and equality. It could lead to bizarre situations where sorting a collection of them could lead to "equal" things being nowhere near each other :-) – paxdiablo Jul 03 '21 at 02:10
  • 4
    @paxdiablo I guess you don't "trust" IEEE-754 format floating point numbers then :) – alephzero Jul 03 '21 at 10:16
  • @alephzero Rust doesn't trust them either. – wizzwizz4 Jul 03 '21 at 22:07
  • 1
    Great observation. [UTS#10 on the *Unicode Collation Algorithm*](https://unicode.org/reports/tr10/) takes some trouble to distinguish collation from comparison and to explain why this matters, including in section [A.3 Deterministic Comparison](https://unicode.org/reports/tr10/#Deterministic_Comparison) and in section [11.1 Collation Folding](https://unicode.org/reports/tr10/#Collation_Folding). – tchrist Jul 04 '21 at 03:07
6

Because == can sometimes be implemented faster than using a <=> b == 0, so the compiler refuses to use potentially suboptimal implementation by default.

E.g. consider std::string, which can check if sizes are the same before looping over the elements.

Note that you don't have to implement == manually. You can =default it, which will implement it in terms of <=>.

Also note that if you =default <=> itself, then =defaulting == is not necessary.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 1
    Same question as for @errorika: Correct, slow code should be compilable. I would think that a warning is sufficient. – einpoklum Jul 02 '21 at 15:40
  • @einpoklum The standard doesn't really have a concept of warnings. It can't mandate them. – HolyBlackCat Jul 02 '21 at 16:53
  • Be that as it may - I see no reason in your answer for == to be _required_, rather than recommended. – einpoklum Jul 02 '21 at 17:01
  • @einpoklum What is your question? In this case the commitette decided to reject potentially slow code, so that's what we have now. – HolyBlackCat Jul 02 '21 at 17:07
  • The question is _why_? C++ rarely, if ever, rejects otherwise valid code because it might not be fast. – einpoklum Jul 02 '21 at 18:03
  • I'm confident that the only reason is performance. If you think there's something else, you can try to dig up the proposal... – HolyBlackCat Jul 02 '21 at 18:04
3

No, you don't. Just add

  bool operator==(A const& other) const = default;

https://godbolt.org/z/v1WGhxca6

You can always overload them to different semantics. To prevent unexpected auto generated function, the = default is needed

J-16 SDiZ
  • 26,473
  • 4
  • 65
  • 84