44
votes
#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?

5
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
Ya'know... that second duplicate I linked is also asked by you....StoryTeller - Unslander Monica
@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
This question is a duplicate of this other question: 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

5 Answers

51
votes

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).
10
votes

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.

7
votes

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.

6
votes

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.

3
votes

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