Spaceship Operator <=> (C++20)
C++20 three-way comparison operator — ordering categories, defaulted synthesis, heterogeneous comparison, and migrating legacy relational operators.
Spaceship Operator (operator<=>)since C++20operator<=> performs a single three-way comparison and returns an ordering category object from <compare> that encodes whether the left operand is less than, equal to, or greater than the right; defaulting it causes the compiler to synthesise all six relational operators member-wise.
Overview
Before C++20, providing a complete, consistent set of relational operators required writing six separate functions — tediously symmetric, prone to inconsistency, and impossible to enforce at the type level. The three-way comparison operator (<=>) collapses that into a single declaration. When defaulted, it compares non-static data members in declaration order, lexicographically, using each member's own <=>.
Unlike the int-returning convention of C's qsort comparator, <=> encodes the kind of ordering in its return type through distinct category types from <compare>. This makes it a type error to use a partial_ordering where a strong_ordering is expected, catching semantic mistakes at compile time.
Syntax
#include <compare>
// Defaulted — member-wise comparison, return type deduced
auto operator<=>(const T& rhs) const = default;
// Explicit return type — override or restrict deduced ordering
std::strong_ordering operator<=>(const T& rhs) const noexcept;
// Heterogeneous — different right-hand-side type
std::strong_ordering operator<=>(std::string_view rhs) const noexcept;Ordering Categories
<=> returns one of three category types from <compare>. Each supports comparison against the integer literal 0 only — comparing against -1, 1, or any other non-zero value always yields false.
std::strong_ordering (C++20)
Equivalent elements are identical — substitutability holds in all contexts. Use for integers, pointers into the same array, and types where all data members contribute to identity.
static_assert((3 <=> 5) == std::strong_ordering::less);
static_assert((5 <=> 5) == std::strong_ordering::equal);
static_assert((7 <=> 5) == std::strong_ordering::greater);std::weak_ordering (C++20)
Equivalent elements may not be identical — equivalence classes exist but substitutability is not guaranteed. The canonical case is case-insensitive string comparison: "ABC" and "abc" are equivalent but not the same string.
struct CIString {
std::string s;
std::weak_ordering operator<=>(const CIString& o) const {
auto lower = [](std::string t) {
for (char& c : t)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return t;
};
return lower(s) <=> lower(o.s);
}
bool operator==(const CIString& o) const { return (*this <=> o) == 0; }
};
CIString a{"Hello"}, b{"hello"};
assert((a <=> b) == std::weak_ordering::equivalent); // same class, different stringsstd::partial_ordering (C++20)
Some pairs are unordered — neither less than, equal to, nor greater than each other. IEEE 754 NaN is the canonical example; a subset relation on sets is another.
double nan = std::numeric_limits<double>::quiet_NaN();
auto r = (1.0 <=> nan);
assert(r == std::partial_ordering::unordered);
assert(!(r < 0) && !(r == 0) && !(r > 0)); // none of the three holdFor a defaulted <=>, the compiler deduces the weakest ordering across all member types. One double member pulls the entire struct to partial_ordering.
Defaulted operator<=> and operator== Synthesis
The synthesis rule hinges on whether the body is defaulted or user-provided — not on the ordering category returned.
A defaulted operator<=> causes the compiler to also declare a defaulted operator== (member-wise equality, not routed through <=>), unless operator== is already user-declared. This holds for all ordering categories, including partial_ordering.
A user-defined (non-defaulted) operator<=> synthesises nothing — you must write operator== yourself.
// Defaulted: both <=> and == are generated // C++20
struct A {
int x, y;
auto operator<=>(const A&) const = default;
};
A a{1, 2}, b{1, 2};
assert(a == b); // OK — synthesised member-wise equality
// User-defined body: == must be explicit
struct B {
int x, y;
std::strong_ordering operator<=>(const B& o) const noexcept {
if (auto c = x <=> o.x; c != 0) return c;
return y <=> o.y;
}
bool operator==(const B& o) const noexcept { return x == o.x && y == o.y; }
};To override the deduced ordering when a float member is irrelevant for ordering:
struct Metric {
int id;
double value; // irrelevant for ordering purposes
std::strong_ordering operator<=>(const Metric& o) const noexcept {
return id <=> o.id;
}
bool operator==(const Metric& o) const noexcept { return id == o.id; }
};Examples
Semantic versioning
#include <compare>
struct Version {
int major, minor, patch;
constexpr std::strong_ordering operator<=>(const Version& o) const noexcept {
if (auto c = major <=> o.major; c != 0) return c;
if (auto c = minor <=> o.minor; c != 0) return c;
return patch <=> o.patch;
}
constexpr bool operator==(const Version&) const noexcept = default;
};
static_assert(Version{1, 2, 3} < Version{1, 3, 0});
static_assert(Version{2, 0, 0} > Version{1, 9, 9});
static_assert(Version{1, 0, 0} == Version{1, 0, 0});
static_assert(Version{1, 2, 3} != Version{1, 2, 4});Heterogeneous comparison
Define <=> with a different RHS type; the compiler generates reversed and symmetric rewritten candidates automatically.
#include <compare>
#include <string>
#include <string_view>
struct InternedString {
std::string data;
std::strong_ordering operator<=>(std::string_view sv) const noexcept {
return data <=> sv;
}
bool operator==(std::string_view sv) const noexcept { return data == sv; }
// Compiler generates reversed candidates via rewriting:
// sv <=> InternedString → 0 @ (InternedString <=> sv)
// sv == InternedString → InternedString == sv
};
InternedString s{"hello"};
assert(s < "world");
assert("hello" == s); // rewritten: s == "hello"
assert(std::string_view{"abc"} < s); // reversed rewriteNamed predicates and std::compare_three_way (C++20)
#include <compare>
#include <algorithm>
#include <vector>
// As an algorithm comparator
std::vector<int> v{3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), std::compare_three_way{});
// Named predicates — clearer in generic code than comparing against 0
auto r = (v[0] <=> v[1]);
std::is_lt(r); // r < 0
std::is_eq(r); // r == 0
std::is_gt(r); // r > 0
std::is_lteq(r); // r <= 0
std::is_gteq(r); // r >= 0
std::is_neq(r); // r != 0
// Query result type statically without calling the operator
using Ord = std::compare_three_way_result_t<int, double>; // partial_orderinglexicographical_compare_three_way (C++20)
#include <algorithm>
#include <compare>
#include <vector>
std::vector<int> a{1, 2, 3};
std::vector<int> b{1, 2, 4};
auto r = std::lexicographical_compare_three_way(
a.begin(), a.end(), b.begin(), b.end());
// r == std::strong_ordering::less — uses element type's <=>
assert(r < 0);Migration from 6-operator boilerplate
C++17 and earlier
struct Record {
int id;
std::string name;
bool operator==(const Record& o) const { return id == o.id && name == o.name; }
bool operator!=(const Record& o) const { return !(*this == o); }
bool operator< (const Record& o) const {
return id != o.id ? id < o.id : name < o.name;
}
bool operator<=(const Record& o) const { return !(o < *this); }
bool operator> (const Record& o) const { return o < *this; }
bool operator>=(const Record& o) const { return !(*this < o); }
};C++20
struct Record {
int id;
std::string name;
auto operator<=>(const Record&) const = default;
// All 6 operators available; operator== also synthesised
};Migration checklist:
- Replace all six relational operators with
auto operator<=>(const T&) const = default;. - Remove the explicit
operator==— it is synthesised automatically when<=>is defaulted. - If float/double members exist but you only care about strong ordering for specific fields, write a custom
<=>with explicitstd::strong_orderingreturn type and addoperator==manually. - Remove coexisting legacy relational operators — they create two code paths;
<=>rewritten candidates take precedence for relational expressions but legacy overloads still participate in direct member-function calls.
Best Practices
- Start with
= default: prefer the defaulted form; write a custom body only when member-wise ordering is semantically wrong. - Annotate
constexpr noexcepton custom bodies: the compiler cannot infer these for non-defaulted implementations. - Use named predicates in generic code:
std::is_lt(r)reads better thanr < 0and works uniformly across all three ordering categories. - Don't model non-total-order relations with
<=>: subset containment, dominance hierarchies, and other partial relations should use named methods likeis_subset_of()rather than<=>, to avoid confusing callers who expect total ordering semantics from standard comparison operators.
Common Pitfalls
// 1. Comparing result against non-zero — always false
auto r = (1 <=> 2);
bool wrong = (r == -1); // always false — only 0 is the valid sentinel
bool right = (r < 0); // correct: strong_ordering::less < 0
// 2. User-defined <=> does not synthesise == — must be explicit
struct Fraction {
int num, den;
std::strong_ordering operator<=>(const Fraction& o) const noexcept {
return static_cast<long long>(num) * o.den
<=> static_cast<long long>(o.num) * den;
}
// Without this, (a == b) fails to compile:
bool operator==(const Fraction& o) const noexcept { return (*this <=> o) == 0; }
};
// 3. Defaulted <=> with any float member deduces partial_ordering
struct S {
int id;
double weight;
auto operator<=>(const S&) const = default; // deduces partial_ordering
};
// operator== IS synthesised (defaulted <=> always synthesises ==),
// but callers expecting strong_ordering for sort keys will be surprised.
// 4. Coexisting legacy operator< and operator<=> creates two code paths
struct Legacy {
int v;
bool operator<(const Legacy& o) const { return v < o.v; } // old
std::strong_ordering operator<=>(const Legacy& o) const noexcept { return v <=> o.v; }
};
Legacy a{1}, b{2};
bool x = a < b; // uses <=> rewrite — ignores legacy operator<
bool y = a.operator<(b); // explicit call: still uses legacy overload
// Solution: remove operator< entirely.
// 5. Domain relations that are not total orders do not belong in operator<=>
// Use is_subset_of(), dominates(), etc. instead of <=> for partial relations.See Also
std::strong_ordering— substitutable total orderstd::weak_ordering— equivalence-class total orderstd::partial_ordering— order with unordered pairs- Operator Overloading — general overloading rules
- Ranges — C++20 range algorithms using three-way comparison