Spaceship operator <=> in C++

Internship at OpenGenus

Get FREE domain for 1st year and build your brand new site

The three-way comparison operator <=>, colloquially called the spaceship operator was added in C++20.

Comparison

Comparison is one of the most commonly used operations in a program. Comparing the built-in types like int's is defined by the language. However, comparison of user-defined types is provided by means of operator overloading.

When we compare objects A and B we are actually calling the operators with parameter. So something like A<B might evaluate as A.operator<(B). Ofcourse the operator<() has to be defined.

Pre-C++20

C++ comparison operators include ==, !=, <, <=, >, and >=. The first two are used to determine if two objects are equal or unequal, and the other four are used to order the objects.

For user-defined objects, use of an expression like A@B where @ is any comparison operator, the overload resolution looks for the best matching candidate for this. First all the member functions, then all the non-member functions and finally all the built-in candidates are checked (in that order) and the best candidate among these is chosen.

Operators are usually defined in terms of others. One common idiom is to use the operator<() to implement the other comparison operators and operator==() to implement the != operator.

Use of < to define the others may work for integers but poses problems for floating points, particularly the NaN which must be false for each one of these comparisons: 1.0f < NaN, 1.0 == NaN, 1.0 > NaN and also for <= and >=. But if 1.f <= NaN is false, then !(NaN < 1.f) is incorrectly determined to be true.

It’s recommended to write comparisons as non-member functions to support heterogeneous comparison. Comparisons like a == 4 and 4 == a should mean the same thing. To achieve this it is recommended to write comparisons as non-member functions and make them friends for convenience.

A new operator <=>

The operator<=> is the new ordering primitive. For two objects A and B, it determines if A<B, A==B or A>B. It returns an object that compares <0 if A<B, compares ==0 if A==B, and compares >0 if A>B.

The comparison is thus three-ways and hence the name of the operator. C++ already has had such a function that compares three-way: basic_string compare() function that lexicographically compares two strings and returns a positive, 0 or a negative value.

The operator<=>() returns an object that belongs to one of the comparison categories. The comparison categories are strong_ordering, weak_ordering and partial_ordering.

For strong_ordering, the values are strong_ordering::less, strong_ordering::equal and strong_ordering::greater. The strong_ordering::equal implies substitutability. i.e., if (a <=> b) == strong_ordering::equal then for functions f, f(a) == f(b). The function usually is for equality of the value of the type such as elements of the vector type.

For weak_ordering, the values are weak_ordering::less, weak_ordering::equal and weak_ordering::greater. The weak_ordering implies equivalence class. For example in case-insensitive string comparison, "str" and "Str" may be weak_ordering::equivalent but not actually equal.

For partial_ordering, the values are partial_ordering::less, partial_ordering::equal, partial_ordering::greater and an additional partial_ordering::unordered.

Primary and secondary operators

The operator<=>() is now considered to be a primary operator to define the secondary operators. This taxonomy is a result of the idiom that authors used operator<() to define other operators, but this had problems as described in the previous section. So the operator<() is not a primary operator anymore. The primary and secondary operators are given in the table below.
operator_taxonomy

This taxonomy allows us to define two properties: reversibility and rewritability.

Primary operators are reversible

Expressions like a==2 and 2==a must be the same. But the latter gives an error in C++17. For this to be legible we need to write operator==(int, A) and make it a non-member friend.
Equality should be symmetric. i.e., a==2 and 2==a must be the same. In C++20 comparison of 2==a evaluates to a.operator==(2), thus the language understands that equality must be symmetric.

Following this, for the operator<=>(), a <=> 4 works and evaluates as a.operator<=>(4) while 4 <=> a would have been considered to be ill-formed in C++17 (if the operator had existed then). In C++20, operator<=> is symmetric as well. Overload resolution for 4 <=> a will find the member function operator<=>(A, int) and consider a synthetic candidate operator<=>(int, A). This reversed candidate will be selected by the overload resolution.

However, 4 <=> a does not evaluate as a.operator<=>(4). Instead, it evaluates as 0 <=> a.operator<=>(4). Its important to note that no actual new functions are generated by the compiler. The expressions are simply rewritten in terms of the reversed candidates.

Secondary operators are rewritable

If we define only operator == and not != then an expression like a != 4 will be considered ill-formed in C++17. But in C++20, expressions containing secondary comparison operators will also try to look up their corresponding primary operators and write the secondary comparison in terms of the primary comparison operator.

The expression a != 4 can be written as !(a == 4). The language understands that and thus so this expression will look up the operator!=() and also operator==(). Here a!=4 will thus evaluate as !a.operator==(4)

Similarly the ordering secondary operators are also rewritten. a @ b gets evaluated as (a <=> b) @ 0. For example: b < 4 is evaluated as b.operator<=>(4) < 0.

In these cases too the no new operators are generated but only evaluated differently.

Using the default

To write all comparisons for user-defined type, we just need to write the operator<=>() that returns the appropriate category type. This is less error-prone and allows us to write easily understandable code.

To be specific we only need to write the primary operators, for example:

struct A{
  T t; U u; //...
  
  bool operator==( A const& rhs) const {
    // ...
  }
  
  strong_ordering operator<=>( A const& rhs) const {
    // ...
  }
};

which gives us a lot less code than writing all the operators.
In C++20 since we’re just doing the default member-wise lexicographical comparison. we can use the default and let the compiler generate them.

struct A {
  T t; U u; //...
  
  bool operator==(A const& rhs) const = default;
  strong_ordering operator<=>(A const& rhs) const = default;
};

or further simplified to get both defaulted operator==() and defaulted operator<=>().

struct A {
  T t; U u; //...
  
  auto operator<=>(A const& rhs) const = default;
};

Example

#include <compare>
#include <iostream>
using namespace std;

class Point {
  int x;
  int y;
  public:
    Point(int x, int y):x(x), y(y){}
  auto operator<=>(const Point&) const = default;
};

int main() {
  Point p1(2, 2), p2(4, 3);
  
  cout << boolalpha << (p1 == p2) << endl; // false
  cout << (p1 != p2) << endl; // true
  
  cout << (p1 <  p2) << endl; // true
  cout << (p1 <= p2) << endl; // true
  cout << (p1 >  p2) << endl; // false
  cout << (p1 >= p2) << endl; // false
  
  return 0;
}

In this example, the default comparison operators are also generated by the compiler.

Note that the equality operator is also generated. Equality is defined to be the equality of members and base classes. In this case of Points, the corresponding values of x and y are compared.

Conclusion

The new addition of the spaceship operator has considerably changed how the comparison is viewed. It has affected how comaprison semantics work at the language level.

Rohit Topi

Intern at OpenGenus | B.Tech Computer Science Student at KLS Gogte Institute Of Technology Belgaum | Contributor to OpenGenus book: "Binary Tree Problems: Must for Interviews and Competitive Coding"

Read More

Improved & Reviewed by:


OpenGenus Foundation OpenGenus Foundation