What is the <=> ("spaceship", three-way comparison) operator in C++?
Asked Answered
Z

6

307

While I was trying to learn about C++ operators, I stumbled upon the following table that listed a strange comparison operator. What does this <=> operator do?

2017 image of a table from cppreference.com Since 2017 cppreference.com updated that page and now contains detailed information about the<=>operator.

Zurkow answered 24/11, 2017 at 4:26 Comment(0)
Z
160

On 2017-11-11, the ISO C++ committee adopted Herb Sutter's proposal for the <=> "spaceship" three-way comparison operator as one of the new features that were added to C++20. In the paper titled Consistent comparison Sutter, Maurer and Brown demonstrate the concepts of the new design. For an overview of the proposal, here's an excerpt from the article:

The expression a <=> b returns an object that compares <0 if a < b, compares >0 if a > b, and compares ==0 if a and b are equal/equivalent.

Common case: To write all comparisons for your type X with type Y, with memberwise semantics, just write:

auto X::operator<=>(const Y&) =default;

Advanced cases: To write all comparisons for your type X with type Y, just write operator<=> that takes a Y, can use =default to get memberwise semantics if desired, and returns the appropriate category type:

  • Return an _ordering if your type naturally supports <, and we’ll efficiently generate symmetric <, >, <=, >=, ==, and !=; otherwise return an _equality, and we’ll efficiently generate symmetric == and !=.
  • Return strong_ if for your type a == b implies f(a) == f(b) (substitutability, where f reads only comparison-salient state that is accessible using the public const members), otherwise return weak_.
Comparison Categories

Five comparison categories are defined as std:: types, with the following predefined values:

┌──────────────────┬───────────────────────────────────┬─────────────┐
│                  │          Numeric  values          │ Non-numeric │
│     Category     ├──────┬────────────┬───────────────┤             │
│                  │ -1   │ 0          │ +1            │   values    │
├──────────────────┼──────┼────────────┼───────────────┼─────────────┤
│ strong_ordering  │ less │ equal      │ greater       │             │
│ weak_ordering    │ less │ equivalent │ greater       │             │
│ partial_ordering │ less │ equivalent │ greater       │ unordered   │
│ strong_equality  │      │ equal      │ nonequal      │             │
│ weak_equality    │      │ equivalent │ nonequivalent │             │
└──────────────────┴──────┴────────────┴───────────────┴─────────────┘

Implicit conversions between these types are defined as follows:

  • strong_ordering with values {less, equal, greater} implicitly converts to:
    • weak_ordering with values {less, equivalent, greater}
    • partial_ordering with values {less, equivalent, greater}
    • strong_equality with values {unequal, equal, unequal}
    • weak_equality with values {nonequivalent, equivalent, nonequivalent}
  • weak_ordering with values {less, equivalent, greater} implicitly converts to:
    • partial_ordering with values {less, equivalent, greater}
    • weak_equality with values {nonequivalent, equivalent, nonequivalent}
  • partial_ordering with values {less, equivalent, greater, unordered} implicitly converts to:
    • weak_equality with values {nonequivalent, equivalent, nonequivalent, nonequivalent}
  • strong_equality with values {equal, unequal} implicitly converts to:
    • weak_equality with values {equivalent, nonequivalent}
Three-way comparison

The<=>token is introduced. The character sequence<=>tokenizes to<= >, in old source code. For example,X<&Y::operator<=>needs to add a space to retain its meaning.

The overloadable operator<=>is a three-way comparison function and has precedence higher than< and lower than<<. It returns a type that can be compared against literal0but other return types are allowed such as to support expression templates. All<=>operators defined in the language and in the standard library return one of the 5 aforementionedstd::comparison category types.

For language types, the following built-in<=>same-type comparisons are provided. All are constexpr, except where noted otherwise. These comparisons cannot be invoked heterogeneously using scalar promotions/conversions.

  • Forbool, integral, and pointer types,<=>returnsstrong_ordering.
  • For pointer types, the different cv-qualifications and derived-to-base conversions are allowed to invoke a homogeneous built-in<=>, and there are built-in heterogeneousoperator<=>(T*, nullptr_t). Only comparisons of pointers to the same object/allocation are constant expressions.
  • For fundamental floating point types,<=> returnspartial_ordering, and can be invoked heterogeneously by widening arguments to a larger floating point type.
  • For enumerations,<=> returns the same as the enumeration's underlying type's<=>.
  • Fornullptr_t,<=> returnsstrong_orderingand always yieldsequal.
  • For copyable arrays,T[N] <=> T[N]returns the same type asT's<=>and performs lexicographical elementwise comparison. There is no<=>for other arrays.
  • Forvoidthere is no<=>.

To better understand the inner workings of this operator, please read the original paper. This is just what I've found out using search engines. Also, check out Comparison Operators, including Three-Way Comparison(C++20).

Zurkow answered 24/11, 2017 at 4:32 Comment(3)
As if cpp wasn't complex enough already. Why not simply write a comparison method...Ito
@Ito The spaceship operator is that comparison method. Additionally, it Just Works and writes (or deletes) the six other comparison operators. I’ll take one comparison operator function written over six individual boilerplates.Retroact
Note that the _equality types died: it turned out that <=> plays well with the four relational operators but not as well with the two equality operators (although there’s some intense syntactic sugar to support the common case where you want all of them).Defecate
R
227

This is called the three-way comparison operator.

According to the P0515 paper proposal:

There’s a new three-way comparison operator, <=>. The expression a <=> b returns an object that compares <0 if a < b, compares >0 if a > b, and compares ==0 if a and b are equal/equivalent.

To write all comparisons for your type, just write operator<=> that returns the appropriate category type:

  • Return an _ordering if your type naturally supports <, and we’ll efficiently generate <, >, <=, >=, ==, and !=; otherwise return an _equality, and we’ll efficiently generate == and !=.

  • Return strong if for your type a == b implies f(a) == f(b) (substitutability, where f reads only comparison-salient state accessible using the nonprivate const interface), otherwise return weak.

The cppreference says:

The three-way comparison operator expressions have the form

lhs <=> rhs   (1)  

The expression returns an object that

  • compares <0 if lhs < rhs
  • compares >0 if lhs > rhs
  • and compares ==0 if lhs and rhs are equal/equivalent.
Rotman answered 24/11, 2017 at 4:38 Comment(10)
For those who are confused (like I was) about what "compares <0", "compares >0", and "compares ==0" means, they mean the <=> returns a negative, positive, or zero value, depending on the arguments. Much like strncmp and memcmp.Abridge
The first bullet-point says that if < is defined then the compiler will generate == - but how is that possible? 'a' < 'b' == 1, but both 'a' < 'a' and 'c' < 'a' are both == 0 so it shouldn't be possible to derive a comparison to be used by == without further information.Bimonthly
@Bimonthly even though both 'a' < 'a' and 'c' < 'a' are both false, 'a' < 'a' and 'a' < 'c' are not. IN strong ordering following is true: a != ba < b || b < aRichards
@Richards Ah, so it can be defined/generated as operator==(T x, T y) { return !(x < y) && !(y < x); } and operator!=(T x, T y) { return (x < y) || (y < x); } - ah-ha! Of course this is less efficient than a true == as it invokes the comparison twice, but still neat.Bimonthly
What do "return strong" and "return weak" mean?Mccarthyism
What does compares <0 mean? Can you give a use-case example?Birdwell
@Birdwell it means that the object returns, when compared < 0 evaluates to true. That is, if a < b then (a <=> b) < 0 is always true.Lyndialyndon
@Birdwell I had the same question! https://mcmap.net/q/16691/-what-does-quot-compares-less-than-0-quot-meanZurkow
So a <=> 0 is the sign function?Chamomile
@Mccarthyism it is not "return strong" or "return weak". It is "strong ordering" and "weak ordering". OT, but the former means a "transitive none-reflective relation", while the later means "transitive reflective relation". Refer to "relation" in modern algebra.Barra
Z
160

On 2017-11-11, the ISO C++ committee adopted Herb Sutter's proposal for the <=> "spaceship" three-way comparison operator as one of the new features that were added to C++20. In the paper titled Consistent comparison Sutter, Maurer and Brown demonstrate the concepts of the new design. For an overview of the proposal, here's an excerpt from the article:

The expression a <=> b returns an object that compares <0 if a < b, compares >0 if a > b, and compares ==0 if a and b are equal/equivalent.

Common case: To write all comparisons for your type X with type Y, with memberwise semantics, just write:

auto X::operator<=>(const Y&) =default;

Advanced cases: To write all comparisons for your type X with type Y, just write operator<=> that takes a Y, can use =default to get memberwise semantics if desired, and returns the appropriate category type:

  • Return an _ordering if your type naturally supports <, and we’ll efficiently generate symmetric <, >, <=, >=, ==, and !=; otherwise return an _equality, and we’ll efficiently generate symmetric == and !=.
  • Return strong_ if for your type a == b implies f(a) == f(b) (substitutability, where f reads only comparison-salient state that is accessible using the public const members), otherwise return weak_.
Comparison Categories

Five comparison categories are defined as std:: types, with the following predefined values:

┌──────────────────┬───────────────────────────────────┬─────────────┐
│                  │          Numeric  values          │ Non-numeric │
│     Category     ├──────┬────────────┬───────────────┤             │
│                  │ -1   │ 0          │ +1            │   values    │
├──────────────────┼──────┼────────────┼───────────────┼─────────────┤
│ strong_ordering  │ less │ equal      │ greater       │             │
│ weak_ordering    │ less │ equivalent │ greater       │             │
│ partial_ordering │ less │ equivalent │ greater       │ unordered   │
│ strong_equality  │      │ equal      │ nonequal      │             │
│ weak_equality    │      │ equivalent │ nonequivalent │             │
└──────────────────┴──────┴────────────┴───────────────┴─────────────┘

Implicit conversions between these types are defined as follows:

  • strong_ordering with values {less, equal, greater} implicitly converts to:
    • weak_ordering with values {less, equivalent, greater}
    • partial_ordering with values {less, equivalent, greater}
    • strong_equality with values {unequal, equal, unequal}
    • weak_equality with values {nonequivalent, equivalent, nonequivalent}
  • weak_ordering with values {less, equivalent, greater} implicitly converts to:
    • partial_ordering with values {less, equivalent, greater}
    • weak_equality with values {nonequivalent, equivalent, nonequivalent}
  • partial_ordering with values {less, equivalent, greater, unordered} implicitly converts to:
    • weak_equality with values {nonequivalent, equivalent, nonequivalent, nonequivalent}
  • strong_equality with values {equal, unequal} implicitly converts to:
    • weak_equality with values {equivalent, nonequivalent}
Three-way comparison

The<=>token is introduced. The character sequence<=>tokenizes to<= >, in old source code. For example,X<&Y::operator<=>needs to add a space to retain its meaning.

The overloadable operator<=>is a three-way comparison function and has precedence higher than< and lower than<<. It returns a type that can be compared against literal0but other return types are allowed such as to support expression templates. All<=>operators defined in the language and in the standard library return one of the 5 aforementionedstd::comparison category types.

For language types, the following built-in<=>same-type comparisons are provided. All are constexpr, except where noted otherwise. These comparisons cannot be invoked heterogeneously using scalar promotions/conversions.

  • Forbool, integral, and pointer types,<=>returnsstrong_ordering.
  • For pointer types, the different cv-qualifications and derived-to-base conversions are allowed to invoke a homogeneous built-in<=>, and there are built-in heterogeneousoperator<=>(T*, nullptr_t). Only comparisons of pointers to the same object/allocation are constant expressions.
  • For fundamental floating point types,<=> returnspartial_ordering, and can be invoked heterogeneously by widening arguments to a larger floating point type.
  • For enumerations,<=> returns the same as the enumeration's underlying type's<=>.
  • Fornullptr_t,<=> returnsstrong_orderingand always yieldsequal.
  • For copyable arrays,T[N] <=> T[N]returns the same type asT's<=>and performs lexicographical elementwise comparison. There is no<=>for other arrays.
  • Forvoidthere is no<=>.

To better understand the inner workings of this operator, please read the original paper. This is just what I've found out using search engines. Also, check out Comparison Operators, including Three-Way Comparison(C++20).

Zurkow answered 24/11, 2017 at 4:32 Comment(3)
As if cpp wasn't complex enough already. Why not simply write a comparison method...Ito
@Ito The spaceship operator is that comparison method. Additionally, it Just Works and writes (or deletes) the six other comparison operators. I’ll take one comparison operator function written over six individual boilerplates.Retroact
Note that the _equality types died: it turned out that <=> plays well with the four relational operators but not as well with the two equality operators (although there’s some intense syntactic sugar to support the common case where you want all of them).Defecate
R
29

Defaulting <=> automatically gives ==, !=, <, >, <=, >= for free

C++20 has a new "default comparison" feature setup so that defaulting <=> gives all the others for free. I believe that this has been the major motivation behind the addition of operator<=>.

Adapted from https://en.cppreference.com/w/cpp/language/default_comparisons:

main.cpp

#include <cassert>
#include <compare>
#include <set>

struct Point {
    int x;
    int y;
    auto operator<=>(const Point&) const = default;
};

int main() {
    Point pt1{1, 1}, pt2{1, 2};

    // Just to show it Is enough for `std::set`.
    std::set<Point> s;
    s.insert(pt1);

    // All of these are automatically defined for us!
    assert(!(pt1 == pt2));
    assert( (pt1 != pt2));
    assert( (pt1 <  pt2));
    assert( (pt1 <= pt2));
    assert(!(pt1 >  pt2));
    assert(!(pt1 >= pt2));
}

compile and run:

sudo apt install g++-10
g++-10 -ggdb3 -O0 -std=c++20 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

An equivalent more explicit version of the above would be:

struct Point {
    int x;
    int y;
    auto operator<=>(const Point& other) const {
        if (auto cmp = x <=> other.x; cmp != 0)
            return cmp;
        return y <=> other.y;
    }
    bool operator==(const Point& other) const = default;
};

In this case, we need to explicitly set bool operator==(const Point& other) const = default; because if operator<=> is not defaulted (e.g. as given explicitly above), then operator== is not automatically defaulted:

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.

The above example uses the same algorithm as the default operator<=>, as explained by cppreference as:

The default operator<=> performs lexicographical comparison by successively comparing the base (left-to-right depth-first) and then non-static member (in declaration order) subobjects of T to compute <=>, recursively expanding array members (in order of increasing subscript), and stopping early when a not-equal result is found

Before C++20, you could not do something like operator== = default, and defining one operator would not lead to the others being defined, e.g. the following fails to compile with -std=c++17:

#include <cassert>

struct Point {
    int x;
    int y;
    auto operator==(const Point& other) const {
        return x == other.x && y == other.y;
    };
};

int main() {
    Point pt1{1, 1}, pt2{1, 2};

    // Do some checks.
    assert(!(pt1 == pt2));
    assert( (pt1 != pt2));
}

with error:

main.cpp:16:18: error: no match for ‘operator!=’ (operand types are ‘Point’ and ‘Point’)
   16 |     assert( (pt1 != pt2));
      |              ~~~ ^~ ~~~
      |              |      |
      |              Point  Point

The above does compile under -std=c++20 however.

Related:

The return type of <=>: std::strong_ordering vs std::weak_ordering vs std::partial_ordering

The return type of <=> is not an int (-1, 0, 1), but rather an object of one of several types, which can then be compared to int, and which give further information about what kind of ordering is implemented notably:

When we gave the implementation:

    auto operator<=>(const Point& other) const {
        if (auto cmp = x <=> other.x; cmp != 0)
            return cmp;
        return y <=> other.y;
    }

auto would have given us std::strong_ordering because that is what <=> returns between two integers as mentioned at https://en.cppreference.com/w/cpp/language/operator_comparison

Otherwise, if the operands have integral type, the operator yields a prvalue of type std::strong_ordering

and this is the semantic that we'd expect for integer points.

partial_ordering is what you get when doing <=> between floats due to the possibility of NaN which is "uncomparable".

TODO concrete examples of weak_ordering and partial_ordering.

Practical meaning of std::strong_ordering and std::weak_ordering suggests that there is currently no example of them making a different in the standard library.

Related: PartialOrdering, StrictWeakOrdering, TotalOrdering, what's the main difference in application

Tested on Ubuntu 20.04, GCC 10.2.0.

Ratable answered 10/11, 2020 at 15:46 Comment(6)
When not defaulting <=> should it give == as well? it happen to overload > and < for me, but yells == operator is missing... when defaulting there's not issue.Montevideo
@TonyTannous that seems to be the expected behaviour according to en.cppreference.com/w/cpp/language/default_comparisons "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." The solution is to add bool operator==(const Point& other) const = default; as in one of my examples.Ratable
Yes, I also found the motivation behind this in p1185r2Montevideo
<=> should be returning some comparison category (in your example, std::strong_ordering makes sense). If you make it return int, it becomes non-composable.Discontinuance
@JanSchultke thanks, I didn't know that., updated trying to fix it. What does non-composable mean? Can you link to an example where it can be seen?Ratable
If you change T to int in this example (godbolt.org/z/9fYoY3jKc), it fails to compile. You need to use the comparison categories for the compiler to be able to compose a defaulted <=> for containing types. std::common_comparison_category doesn't work with int.Discontinuance
R
23

Three-Way Comparison operator (<=>) is introduced in C++ 20.

This expression returns the object as below;

auto cmp  = a <=> b;

cmp > 0 if a > b
cmp = 0 if a == b
cmp < 0 if a < b  

Example Program

#include <iostream>

using namespace std;

int main()
{
        int lhs = 10, rhs = 20;
        auto result = lhs <=> rhs;

        if (result < 0) {
                cout << "lhs is less than rhs" << endl;
        }
        else if (result > 0) {
                cout << "lhs is greater than rhs" << endl;
        }
        else {
                cout << "lhs and rhs are equal" << endl;
        }

}

How to compile and run?

g++-10 threewaycmp.cpp -std=c++20
./a.out

Result

lhs is less than rhs

Please refer to below link for more details https://en.cppreference.com/w/cpp/language/operator_comparison

Rapturous answered 10/7, 2021 at 6:52 Comment(0)
D
11

This answer has become irrelevant since the referenced web page has changed

The web page you are referencing was broken. It was being edited a lot that day and different parts were not in sync. The status when I was looking at it was:

At the top of the page it lists the currently existing comparison operators (in C++14). There is no <=> there.

At the bottom of the page, they should have listed the same operators, but they goofed and added this future suggestion.

gcc doesn't know about <=> yet (and with -std=c++14, never will), so it thinks you meant a <= > b. This explains the error message.

If you try the same thing five years from now you will probably get a better error message, something like <=> not part of C++14.

Demark answered 24/11, 2017 at 8:36 Comment(4)
The web page OP links to is correct, as is the separate page you link to. It qualifies the <=> operator with the (since C++20) label, telling you which version of the standard to expect it in. Standards labeling is a convention that cppreference.com follows. Of course you don't have a compiler that came back in a time machine to support it for you, but cpprefernce tells you (correctly) what to expect.Lastditch
Yes, but... Not an answer. You are commenting... or something.Zurkow
I intended to link to the same web page as the question, but missed. I think I answered the parts of the question other answers didn't. I ignored the main bolded question since other had already answered that.Demark
This answer doesn't attempt to answer the question "What is the <=> operator?" at all. It would have been better as a comment on the question.Discontinuance
A
0

The virtue of <=> is with complex types whose comparison is expensive, for situations like tree navigation. You can "int x = A <=> B;" once and then determine >, <, >=, <=, ==, != from x without additional comparisons of A and B. For a tree, imagine bool find(root, A){ if root is nullptr return false; int x = A <=> root->B; if !x, you found it return true, else if x < 0, find left, else find right}. (Tune for tail recursion.)

I thought some language had a three way control flow for this, like switch, but it is hard to recall via google. It's a situation going back to C strcmp(), memcmp(), continuing with JAVA compareTo(). Crowdsource me on this!

Ation answered 30/8, 2022 at 20:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.