C++ Spaceship Operator: Simplifying Comparisons

The three-way comparison operator, introduced in C++20 and widely known as the spaceship operator (<=>), fundamentally simplifies relational operations by reducing boilerplate code. Instead of requiring developers to manually overload six separate relational operators (==, !=, <, <=, >, and >=), the spaceship operator allows a type to establish its complete ordering semantics in a single function definition. This article explains how the spaceship operator functions, how the compiler synthesizes relational operators from it, and why it leads to cleaner, less error-prone C++ code.

The Problem with Traditional Relational Operators

Prior to C++20, fully supporting relational comparisons for a custom type required defining up to six distinct operator overloads:

bool operator==(const MyType& lhs, const MyType& rhs);
bool operator!=(const MyType& lhs, const MyType& rhs);
bool operator<(const MyType& lhs, const MyType& rhs);
bool operator<=(const MyType& lhs, const MyType& rhs);
bool operator>(const MyType& lhs, const MyType& rhs);
bool operator>=(const MyType& lhs, const MyType& rhs);

Even though operators like !=, <=, >, and >= are typically implemented in terms of == and <, writing and maintaining this boilerplate was tedious and prone to logical inconsistencies. If a developer updated a class's members, every operator implementation had to be manually inspected and updated.

How the Spaceship Operator Works

The three-way comparison operator (<=>) determines whether an operand is less than, equal to/equivalent to, or greater than another operand in a single evaluation. Instead of returning a bool, it returns a comparison category type from the <compare> header:

  • std::strong_ordering: Strict equality where equivalent values are indistinguishable (e.g., standard integers). Yields values such as std::strong_ordering::less, equal, or greater.
  • std::weak_ordering: Equivalence without strict substitutability (e.g., case-insensitive strings where "Hello" and "hello" are equivalent but not identical).
  • std::partial_ordering: Accommodates values that cannot be ordered relative to one another (e.g., IEEE floating-point numbers containing NaN). Yields std::partial_ordering::unordered when values cannot be compared.

Automatic Generation with = default

The greatest simplification comes from defaulting the operator. When operator<=> is set to = default, the compiler automatically conducts a member-wise, lexicographical comparison of the class's subobjects in declaration order:

#include <compare>
#include <string>

struct User {
    int id;
    std::string name;

    // Generates all six relational operators
    auto operator<=>(const User&) const = default;
};

Defaulting operator<=> also automatically synthesizes an optimized operator== with the same member-wise semantics. With that single line of code, expressions like user1 < user2, user1 == user2, and user1 >= user2 become valid.

Compiler Rewriting and Symmetry

The C++20 compiler uses rewrite rules to resolve comparison expressions. If code evaluates a < b, the compiler checks for operator<, but if none is found, it automatically rewrites the expression to:

(a <=> b) < 0

Similarly:

  • a > b is rewritten as (a <=> b) > 0
  • a <= b is rewritten as (a <=> b) <= 0
  • a >= b is rewritten as (a <=> b) >= 0

These rewrite rules also provide symmetry for mixed-type operations. If an operator supports comparing TypeA <=> TypeB, expressions comparing TypeB against TypeA are automatically synthesized by reversing the comparison, removing the need to write separate commutative overloads.

Custom Ordering Logic

When member-wise comparison is insufficient, custom three-way comparisons remain straightforward. Developers only need to define operator<=> and, if necessary, operator==:

#include <compare>

struct CustomInt {
    int value;

    std::strong_ordering operator<=>(const CustomInt& other) const {
        return value <=> other.value;
    }

    bool operator==(const CustomInt& other) const {
        return value == other.value;
    }
};

Separating operator== from operator<=> allows performance optimizations. Equality checks can frequently short-circuit much faster (e.g., checking container sizes before elements) than a full three-way comparison, giving developers fine-grained control when performance demands it.

Key Benefits

  1. Reduced Boilerplate: Replaces six manual overloads with a single defaulted or custom function.
  2. Consistency: Eliminates edge-case bugs caused by asymmetrical or contradictory operator overloads.
  3. Expressive Semantics: Explicitly communicates ordering properties via strong_ordering, weak_ordering, or partial_ordering.
  4. Maintainability: Adding new members to a defaulted struct automatically includes them in comparisons without altering existing code.