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 asstd::strong_ordering::less,equal, orgreater.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 containingNaN). Yieldsstd::partial_ordering::unorderedwhen 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) < 0Similarly:
a > bis rewritten as(a <=> b) > 0a <= bis rewritten as(a <=> b) <= 0a >= bis 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
- Reduced Boilerplate: Replaces six manual overloads with a single defaulted or custom function.
- Consistency: Eliminates edge-case bugs caused by asymmetrical or contradictory operator overloads.
- Expressive Semantics: Explicitly communicates
ordering properties via
strong_ordering,weak_ordering, orpartial_ordering. - Maintainability: Adding new members to a defaulted struct automatically includes them in comparisons without altering existing code.