#include <compare>
#include <iostream>

int main()
{ 
   auto comp1 = 1.1 <=> 2.2;
   auto comp2 = -1 <=> 1;
   std::cout << typeid(comp1).name()<<"\n"<<typeid(comp2).name();
}

Output:

struct std::partial_ordering
struct std::strong_ordering

I know that if the operands have an integral type, the operator returns a PRvalue of type std::strong_ordering. I also know if the operands have a floating-point type, the operator yields a PRvalue of type std::partial_ordering.

But why should I use a three-way comparison operator instead of two-way operators (==, !=, <, <=, >, >=)? Is there an advantage this gives me?

4

There are 4 answers

0
molbdnilo On BEST ANSWER

It makes it possible to determine the ordering in one operation.
The other operators require two comparisons.

Summary of the other operators:

  • If a == b is false, you don't know whether a < b or a > b
  • If a != b is true, you don't know whether a < b or a > b
  • If a < b is false, you don't know whether a == b or a > b
  • If a > b is false, you don't know whether a == b or a < b
  • If a <= b is true, you don't know whether a == b or a < b
  • If a >= b is true, you don't know whether a == b or a > b

A neat side effect is that all the other operators can be implemented in terms of <=>, and a compiler can generate them for you.

Another side effect is that people might be confused by the use of <=> as the equivalence arrow in mathematics, which it has been pretty much since typewriters got those three symbols.
(I'm personally pretty miffed by how a <=> b is "truthy" if and only if a and b are not equivalent.)

0
anastaciu On

The spaceship operator was proposed by Herb Sutter and was adopted by the committee to be implemented with C++ 20, detailed report can be consulted here, or you if are more into lectures, here you can see a video of the man himself making the case for it. In pages 3/4 of the report you can see the main use case:

The comparison operators implementation, needed for pre C++20, in the following class:

class Point
{
    int x;
    int y;

public:
    friend bool operator==(const Point &a, const Point &b) { return a.x == b.x && a.y == b.y; }
    friend bool operator<(const Point &a, const Point &b) { return a.x < b.x || (a.x == b.x && a.y < b.y); }
    friend bool operator!=(const Point &a, const Point &b) { return !(a == b); }
    friend bool operator<=(const Point &a, const Point &b) { return !(b < a); }
    friend bool operator>(const Point &a, const Point &b) { return b < a; }
    friend bool operator>=(const Point& a, const Point& b) { return !(a < b); }
    // ... non-comparisonfunctions ...
};

Would be replaced by:

class Point
{
    int x;
    int y;

public:
    auto operator<=>(const Point &) const = default; 
    // ... non-comparison functions ...
};

So to answer your question, overloading operator<=> as class member allows you to use all comparison operators for the class objects without having to implement them, defaulting it also defaults operator==, if it's not otherwise declared, which in term automatically implements operator!=, making all comparison operations available with a single expression. This functionality is the main use case.

I would like to point out that the spaceship operator would not be possible without the introduction of the default comparison feature with C++20.

Defaulted three-way comparison

[...]
Let R be the return type, each pair of subobjects a, b is compared as follows:
[...]
... if R is std::strong_ordering, the result is:

a == b ? R::equal : a < b ? R::less : R::greater

Otherwise, if R is std::weak_ordering, the result is:

a == b ? R::equivalent : a < b ? R::less : R::greater

Otherwise (R is std::partial_ordering), the result is:

a == b ? R::equal : a < b ? R::less : b < a ? R::greater : R::unordered

Per the rules for any operator<=> overload, a defaulted <=> overload will also allow the type to be compared with <, <=, >, and >=.

If operator<=> is defaulted and operator== is not declared at all, then operator== is implicitly defaulted.

It also allows to default operator == only, which will implement operator !=, albeit not as versatile as the former, it's also an interesting possibility.

0
SergeyA On

The main advantage (at least for me) is the fact that this operator can be defaulted for the class, which will automatically support all possible comparisons for your class. I.e.

#include <compare>

struct foo {
    int a;
    float b;
    auto operator<=>(const foo& ) const = default;
};

// Now all operations used before are defined for you automatically!

auto f1(const foo& l, const foo& r) {
    return l < r;
}

auto f2(const foo& l, const foo& r) {
    return l > r;
}

auto f3(const foo& l, const foo& r) {
    return l == r;
}

auto f4(const foo& l, const foo& r) {
    return l >= r;
}

auto f5(const foo& l, const foo& r) {
    return l <= r;
}

auto f6(const foo& l, const foo& r) {
    return l != r;
}

Previously, all those operations would have to be defined within the class, which is cumbersome and error-prone - as one would have to remember to revisit those whenever new members are added to the class.

3
Nicol Bolas On

Use your own judgment.

The point of the spaceship operator is not specifically for comparing objects. The main point of it is permitting the compiler to synthesize the other comparison operators from the spaceship operator.

If you don't specifically need to answer the question less-than, equal-to, or greater-than, then you don't need to invoke it directly. Use the operator that makes sense for you.

But should you need to make a type comparable, you only have to write 2 functions (spaceship and equality) rather than 6. And when writing such a function, you can use the spaceship operator on the individual types in question (should they be comparable in such a way). That makes it even easier to implement such functions.

The other useful thing that the spaceship operator allows you to do is tell what kind of ordering a comparison will provide. Partial, strong, or whatever. This can theoretically be useful, but it's fairly rare overall.