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.Choosing an Ordering Category
The ordering category you return is a semantic contract, not a performance hint. Choosing the wrong category misleads callers who rely on substitutability guarantees or total-order semantics.
Use std::strong_ordering when equal values are genuinely identical — all data members contribute to identity and there is no observable distinction between two equivalent objects. Integers, pointers into the same array, and plain-data structs whose every field is part of the key all qualify.
Use std::weak_ordering when you define equivalence classes: equal-under-comparison values may differ in observable ways that are irrelevant to ordering. Case-insensitive strings, locale-collated text, and objects compared by a single key field (while retaining unrelated payload fields) belong here.
Use std::partial_ordering only when genuinely unordered pairs exist — values for which none of less, equal, or greater holds. This is rare outside floating-point and explicitly partial mathematical structures such as subset containment. Prefer strong_ordering or weak_ordering wherever the domain allows it, because standard containers and algorithms work best with total orders.
// C++20
#include <compare>
// strong_ordering — every bit contributes to identity
struct Point2D {
int x, y;
auto operator<=>(const Point2D&) const noexcept = default;
// deduces strong_ordering: two Point2D objects are equal iff x==x && y==y
};
// weak_ordering — compared by key only; payload is irrelevant to order
struct Employee {
int id; // ordering key
std::string name; // payload — not part of the order
std::weak_ordering operator<=>(const Employee& o) const noexcept {
return id <=> o.id;
}
bool operator==(const Employee& o) const noexcept { return id == o.id; }
};
// partial_ordering — explicitly permits unordered pairs (e.g. interval overlap check)
struct Interval {
double lo, hi;
std::partial_ordering operator<=>(const Interval& o) const noexcept {
if (hi < o.lo) return std::partial_ordering::less;
if (lo > o.hi) return std::partial_ordering::greater;
if (lo == o.lo && hi == o.hi) return std::partial_ordering::equivalent;
return std::partial_ordering::unordered; // overlapping but not equal
}
bool operator==(const Interval& o) const noexcept {
return lo == o.lo && hi == o.hi;
}
};The implicit conversion hierarchy mirrors mathematical strength: strong_ordering converts to weak_ordering, and weak_ordering converts to partial_ordering. A function accepting partial_ordering accepts all three; the reverse is not true and will not compile.
Heterogeneous Comparison
Defining <=> with a different RHS type enables transparent mixed-type comparison without explicit casts. The compiler's rewrite rules generate reversed and symmetric candidates automatically, so lhs < rhs and rhs < lhs both resolve correctly from a single operator definition.
When writing a heterogeneous <=>, always pair it with a matching heterogeneous operator==. The two are independent overloads — the compiler does not synthesise one from the other unless both are defaulted.
// C++20
#include <compare>
#include <string>
#include <string_view>
struct PathKey {
std::string path;
// Compare against string_view directly — no temporary std::string needed
std::strong_ordering operator<=>(std::string_view sv) const noexcept {
return std::string_view{path} <=> sv;
}
bool operator==(std::string_view sv) const noexcept {
return std::string_view{path} == sv;
}
};
PathKey k{"/usr/lib"};
assert(k < "/var"); // PathKey <=> string_view
assert("/etc" < k); // rewritten: 0 < (k <=> "/etc")
assert(k == "/usr/lib"); // PathKey == string_view
assert("/usr/lib" == k); // rewritten: k == "/usr/lib"
// Numeric heterogeneous comparison — int vs long
struct Index {
int value;
std::strong_ordering operator<=>(long rhs) const noexcept {
return static_cast<long>(value) <=> rhs;
}
bool operator==(long rhs) const noexcept {
return static_cast<long>(value) == rhs;
}
};
Index idx{42};
assert(idx < 100L); // Index <=> long
assert(10L < idx); // rewritten candidateThe rewritten-candidate rule (0 @ (a <=> b)) applies only to <=> and ==. For <, >, <=, >= the compiler rewrites the expression to a <=> call; for == and != it uses operator== directly (with reversal). Keeping <=> and == consistent is therefore essential.
Three-Way Comparison with Floating-Point
Floating-point types return std::partial_ordering because NaN is unordered with every value including itself. This is not an implementation quirk — it is required by IEEE 754, and <=> faithfully models it.
When a struct has a double or float member and uses a defaulted <=>, the entire struct's ordering degrades to partial_ordering. If the float field is irrelevant to ordering, write a custom <=> with an explicit return type and ignore the float member.
// C++20
#include <compare>
#include <limits>
#include <cassert>
// NaN is unordered — none of <, ==, > hold
double nan = std::numeric_limits<double>::quiet_NaN();
double val = 3.14;
auto r1 = (val <=> nan);
auto r2 = (nan <=> nan);
assert(r1 == std::partial_ordering::unordered);
assert(r2 == std::partial_ordering::unordered);
assert(!(r1 < 0) && !(r1 == 0) && !(r1 > 0)); // all three are false
// Defaulted <=> with a float member — whole struct becomes partial_ordering
struct Measurement {
int sensor_id;
double reading;
auto operator<=>(const Measurement&) const = default; // partial_ordering
};
// Custom <=> — order by sensor_id only, ignore the float
struct MeasurementSorted {
int sensor_id;
double reading;
std::strong_ordering operator<=>(const MeasurementSorted& o) const noexcept {
return sensor_id <=> o.sensor_id;
}
bool operator==(const MeasurementSorted& o) const noexcept {
return sensor_id == o.sensor_id;
}
};
// std::partial_order() and std::strong_order() customisation points
// handle floating-point in generic code:
auto po = std::partial_order(1.0, nan); // partial_ordering::unordered
// std::strong_order() uses IEEE 754 totalOrder, treating NaN as ordered:
auto so = std::strong_order(1.0, nan); // strong_ordering::less (NaN > finite)std::strong_order is useful in containers that require total order (such as std::map) when the element type is double — it imposes a consistent total order by treating negative zero as less than positive zero and arranging NaN values at the top.
The <compare> Header and Standard Utilities
Everything in the three-way comparison ecosystem lives in <compare> — including the category types, named predicates, comparison function objects, and customisation-point objects. Including it explicitly documents intent and avoids relying on transitive inclusion from <string> or <vector>.
std::compare_three_way is a transparent function object that calls <=> on its arguments. It satisfies the Compare named requirement and is accepted anywhere a comparator is expected. std::compare_partial_order_fallback extends this to types that do not provide <=> by synthesising partial ordering from < and ==.
// C++20
#include <compare>
#include <algorithm>
#include <functional>
#include <vector>
// compare_three_way as a sort comparator
std::vector<int> v{5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end(), std::compare_three_way{});
// compare_three_way_result_t — query the result type without calling the operator
using R1 = std::compare_three_way_result_t<int, int>; // strong_ordering
using R2 = std::compare_three_way_result_t<double, double>; // partial_ordering
using R3 = std::compare_three_way_result_t<int, double>; // partial_ordering
// compare_partial_order_fallback — works even on types with only < and ==
// (synthesises partial_ordering from those two operators)
struct OldType {
int x;
bool operator<(const OldType& o) const { return x < o.x; }
bool operator==(const OldType& o) const { return x == o.x; }
};
OldType a{1}, b{2};
auto r = std::compare_partial_order_fallback(a, b); // partial_ordering::less
// Named predicate functions — work on all three category types
auto cmp = (3 <=> 5);
static_assert(std::is_lt(cmp)); // true
static_assert(std::is_lteq(cmp)); // true
static_assert(!std::is_gt(cmp)); // false
static_assert(!std::is_eq(cmp)); // falseThe customisation-point objects std::strong_order, std::weak_order, and std::partial_order go further than <=> alone: they consult ADL-found strong_order/weak_order/partial_order overloads first, then fall back to <=>, and finally apply type-specific rules for floating-point. Use them in generic code when you need a guaranteed ordering category regardless of what the type directly exposes.
Interaction with the Standard Library
Once a type provides operator<=>, every standard library algorithm and container that uses ordering works automatically — no extra overloads needed. The standard library uses <=> rewritten candidates for all relational comparisons internally, so std::sort, std::lower_bound, std::set, and std::map all benefit without any change to call sites.
std::sort and its variants require a strict weak order. Any type whose <=> returns std::strong_ordering or std::weak_ordering satisfies this automatically. A partial_ordering type does not satisfy strict weak order and must supply a custom comparator that establishes one.
// C++20
#include <compare>
#include <algorithm>
#include <set>
#include <map>
#include <vector>
struct Priority {
int level;
auto operator<=>(const Priority&) const noexcept = default;
};
// std::sort — uses <=> rewritten candidates
std::vector<Priority> tasks{{3}, {1}, {4}, {1}, {5}};
std::sort(tasks.begin(), tasks.end()); // ascending, no comparator needed
std::sort(tasks.begin(), tasks.end(), std::greater<>{}); // descending
// std::set — uses <=> for the internal red-black tree ordering
std::set<Priority> pset{{2}, {5}, {1}, {3}};
auto it = pset.find({3}); // O(log n) — <=> drives the tree traversal
// std::map — key type only needs <=>; no separate less<> specialisation
std::map<Priority, std::string> labels;
labels[{1}] = "low";
labels[{5}] = "critical";
// std::lower_bound and std::upper_bound — binary search via <=>
std::vector<Priority> sorted{{1}, {2}, {3}, {4}, {5}};
auto lb = std::lower_bound(sorted.begin(), sorted.end(), Priority{3});
// *lb == Priority{3}
// Ranges algorithms — std::ranges::sort uses <=> directly
std::ranges::sort(tasks); // C++20 ranges, no comparator argument neededLegacy code that defines only operator< continues to work with std::sort and std::set, because those use std::less<> which calls operator<. Migrating to <=> is additive: existing containers do not need to be changed, and the new operator is picked up automatically for any relational expression that triggers the rewrite rules.
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