C++ programming language logo, white text 'C++' in shaded blue hexagon
blog

C++: Overloading the Spaceship Operator, A Recipe

How to overload the three-way comparison (spaceship) operator<=>, and a reminder to overload operator== as well.
3 min read / (updated 24 Nov 24)

The three-way comparison operator <=> (colloquially known as the spaceship operator) was introduced in C++20 to reduce boilerplate when overloading comparison operators. What used to look like:

struct A {
  int x;

  bool operator==(const A& o) const {
    return x == o.x;
  }
  bool operator!=(const A& o) const {
    return x != o.x;
  }
  bool operator<(const A& o) const {
    return x < o.x;
  }
  bool operator<=(const A& o) const {
    return x <= o.x;
  }
  bool operator>(const A& o) const {
    return x > o.x;
  }
  bool operator>=(const A& o) const {
    return x >= o.x;
  }
};

can now look like:

struct A {
  int x;

  auto operator<=>(const A& o) const {
    return x <=> o.x;
  }
  bool operator==(const A& o) const {
    return x == o.x;
  }
};

where the operator<=> returns a value according to whether this is less than, equivalent to, or greater than the argument o. There are even default comparisons—ways of declaring default or implicit comparison operators—but in this post we are interested only in how to implement a custom comparison with this new vehicle.

It’s a three step process.

What sort of ordering do you have?

Consider the properties of the custom comparison. Is it a total ordering (e.g. like the set of integers)? A partial ordering (e.g. like the set of nodes in a directed graph)? Pick from one of the following, which will be the return type of the overload:

auto
You totally know what you’re doing, or you totally don’t. More below.
std::strong_ordering
Among sets of comparable objects, there is a total order. Incomparable objects are not allowed. Two objects that are equivalent are also substitutable, in that they behave the same way (likely all members are equivalent, too). Valid return values are:
  • std::strong_ordering::less
  • std::strong_ordering::equivalent
  • std::strong_ordering::greater
std::weak_ordering
Like std::strong_ordering, but two objects that are equivalent are not necessarily substitutable, in that they may not behave the same way (likely the comparison involves only a subset of members, and the other members are allowed to be different). Valid return values are:
  • std::weak_ordering::less
  • std::weak_ordering::equivalent
  • std::weak_ordering::greater
std::partial_ordering
Among sets of comparable objects, there is a partial order. Incomparable objects are allowed, and there is a special return value for them. Like std::weak_ordering, two objects that are equivalent are not necessarily substitutable. Valid return values are:
  • std::partial_ordering::less
  • std::partial_ordering::equivalent
  • std::partial_ordering::greater
  • std::partial_ordering::unordered (the special return value when arguments are incomparable)

Overload operator<=>

Within a class named A, overload the spaceship operator with the likes of:

auto operator<=>(const A& o) const {
  return /* this compared to o is... less? equivalent? greater? */
}

Replace auto with the return type chosen above, and implement the function to return one of the associated valid values as described above. For example, if we wished to explicitly implement the spaceship operator for struct A in the opening example, and choose std::weak_ordering, we would have:

struct A {
  int x;
  
  std::weak_ordering operator<=>(const A& o) {
    if (x < o.x) {
      return std::weak_ordering::less;
    } else if (x == o.x) {
      return std::weak_ordering::equivalent;
    } else {
      return std::weak_ordering::greater;
    }
  }
  bool operator==(const A& o) const {
    return x == o.x;
  }
};

Using auto as the return type is okay if the implementation itself uses the spaceship operator, as in the original code of the opening example:

auto operator<=>(const A& o) const {
  return x <=> o.x;
}

Here, the return type is deduced to be the same as that of the spaceship operator for the type of the member variable x (i.e. int). Now you totally know what you’re doing.

Overload operator==

This is the catch. When operator<=> is defined, the four relational operators operator<, operator<=, operator> and operator>= come with it, but the two equality operators operator== and operator!= do not. Define operator== with something like:

bool operator==(const A& o) const {
  return x == o.x;
}

With this defined, operator!= is also implicitly defined, completing the set of six comparison operators.

Summary

That’s all. The relational operators can be defined with a single definition of operator<=>. The catch is that the equality operators are not defined by this, so a definition of operator== is still required to complete the set.

Here’s complete source code for further exploration. You can comment out the operator<=> or operator== definition to see what compiler errors arise when these are missing.

#include <compare>
#include <iostream>

struct A {
  int x;

  auto operator<=>(const A& o) const {
    return x <=> o.x;
  }
  bool operator==(const A& o) const {
    return x == o.x;
  }
};

int main() {
  A x{1}, y{0};
  std::cerr << (x == y) << ' ';
  std::cerr << (x != y) << ' ';
  std::cerr << (x < y) << ' ';
  std::cerr << (x <= y) << ' ';
  std::cerr << (x > y) << ' ';
  std::cerr << (x >= y) << std::endl;
  return 0;
}