Why should I use the three-way comparison operator (<=>) instead of the two-way comparison operators? Does this have an advantage?
Asked Answered
F

4

14
#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?

Featured answered 27/4, 2021 at 13:49 Comment(5)
Here's something that you can read open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r2.pdf by the big man himself.Leash
It depends on the context I would say. If you're creating a class, implementing operator<=> (or even defaulting if possible) saves time (development and maintenance) instead of having to implement all of them manually. If you on the other hand are interested in knowing if( 1.1 < 2.2 ), then it probably doesn't make much sense to use <=>Fasciate
Less code to write using 2 comparison operators <=> and ==. Less chance of having a mistake/bug than writing 6 comparison operators.Continuant
Are you asking about using <=> to perform a comparison, or about using <=> in any general context?Triplenerved
@DrewDormann Actually bothFeatured
A
13

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

Atc answered 27/4, 2021 at 14:4 Comment(0)
P
11

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.

Prompter answered 27/4, 2021 at 14:1 Comment(0)
D
6

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.

Defamation answered 27/4, 2021 at 14:9 Comment(3)
... and if spaceship is defaulted, you don't even need to write operator==Fasciate
you only have to write 2 functions (spaceship and equality) I wonder whether equality couldn't be based on the spaceship operator as well (like all the other relational operators). I always was under the impression of that.Slier
@Scheff: Only if you = default it. Types where you need to explicitly define a comparison operator usually have short-circuit versions if you only care about equality testing. So if you user-define one, you need to user-define the other one (which you could do in terms of the spaceship easily enough).Defamation
L
5

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.

Leash answered 27/4, 2021 at 14:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.