<compare>
Three-way comparison ordering categories, the <=> spaceship operator, and named comparison helpers introduced in C++20.
<compare>since C++20The <compare> header defines the three comparison category types (std::strong_ordering, std::weak_ordering, std::partial_ordering) and named comparison helpers that support the three-way comparison operator <=>.
Overview
C++20's three-way comparison operator <=> β the spaceship operator β collapses six relational operators into one expression that simultaneously encodes whether the left operand is less than, equal to, or greater than the right. The result is not an integer: it is one of three strongly-typed ordering values defined in <compare>, each modelling a distinct mathematical notion of order.
Comparison Categories
| Type | Semantics | Typical Source |
|---|---|---|
std::strong_ordering | Total order; equal values are substitutable | Integers, enumerations |
std::weak_ordering | Total order; equivalent values may differ in identity | Case-insensitive strings |
std::partial_ordering | Partial order; some pairs may be unordered | Floating-point (NaN) |
Each type exposes static member constants:
// C++20 β std::strong_ordering
std::strong_ordering::less
std::strong_ordering::equal // alias for ::equivalent
std::strong_ordering::equivalent // alias for ::equal
std::strong_ordering::greater
// C++20 β std::weak_ordering
std::weak_ordering::less
std::weak_ordering::equivalent
std::weak_ordering::greater
// C++20 β std::partial_ordering
std::partial_ordering::less
std::partial_ordering::equivalent
std::partial_ordering::greater
std::partial_ordering::unordered // NaN <=> 1.0 produces thisThe three types form an implicit conversion hierarchy: strong_ordering converts to weak_ordering, which converts to partial_ordering. A composite operator<=> that combines member comparisons propagates the weakest category of its members automatically.
Named Comparison Helpers
Six free functions accept any ordering type and return bool, insulating generic code from the specific category:
#include <compare>
auto r = a <=> b;
std::is_lt(r) // a < b
std::is_lteq(r) // a <= b
std::is_eq(r) // a == b
std::is_neq(r) // a != b
std::is_gt(r) // a > b
std::is_gteq(r) // a >= bPrefer these over comparing against ::less or against 0 when writing templates that must handle all three category types.
Syntax
// C++20
auto result = lhs <=> rhs; // return type deduced from operand types
// Comparing against the literal 0 β all ordering types support this
if (result < 0) { /* lhs < rhs */ }
if (result == 0) { /* lhs == rhs */ }
if (result > 0) { /* lhs > rhs */ }The compiler deduces the return type:
- Integer or enumeration operands β
std::strong_ordering - Floating-point operands β
std::partial_ordering - Class types β whatever the class's
operator<=>declares
Examples
Defaulted <=> generates all six operators
A defaulted operator<=> does more than replace operator<. The compiler generates all six relational operators (<, <=, >, >=, ==, !=) from a single declaration. It also implicitly declares and defaults operator==, which means you get equality testing for free alongside ordering β provided the semantics of equality and ordering are both member-wise.
// C++20
#include <compare>
#include <cassert>
struct Point {
int x;
int y;
// One declaration replaces <, <=, >, >=, ==, !=
auto operator<=>(const Point&) const = default;
// operator== is also generated automatically as = default
};
int main() {
Point p1{1, 2};
Point p2{1, 3};
Point p3{1, 2};
assert(p1 < p2); // x equal, y: 2 < 3
assert(p2 > p1); // synthesised
assert(p1 <= p3); // synthesised
assert(p1 >= p3); // synthesised
assert(p1 == p3); // generated separately from <=>
assert(p1 != p2); // synthesised from ==
}The == is generated as a separate defaulted function β not as a special case of <=>. This matters because the language resolves == and != through a dedicated lookup that prefers operator== over <=>. You can independently override equality while keeping the defaulted ordering:
// C++20
struct TaggedPoint {
int x;
int y;
int tag; // metadata β should not affect ordering
// Ordering ignores tag
auto operator<=>(const TaggedPoint& rhs) const {
return std::tie(x, y) <=> std::tie(rhs.x, rhs.y);
}
// Equality also ignores tag β must be written explicitly
// because the non-defaulted <=> above does NOT auto-generate ==
bool operator==(const TaggedPoint& rhs) const {
return x == rhs.x && y == rhs.y;
}
};When operator<=> is non-defaulted (written by hand), the compiler does not synthesise operator== β you must define it yourself.
Heterogeneous comparison
The spaceship operator supports comparing objects of different types. Defining operator<=>(const MyType&, const OtherType&) causes the compiler to generate all six operators for both orderings: MyType op OtherType and OtherType op MyType (the reversed form is synthesised automatically).
// C++20
#include <compare>
#include <string>
#include <string_view>
class MyString {
public:
explicit MyString(std::string s) : data_(std::move(s)) {}
// Heterogeneous: compare MyString against std::string_view directly
std::strong_ordering operator<=>(std::string_view rhs) const {
return data_ <=> rhs;
}
bool operator==(std::string_view rhs) const {
return data_ == rhs;
}
// Homogeneous ordering β lets MyString sort among itself
auto operator<=>(const MyString&) const = default;
private:
std::string data_;
};
void demo() {
MyString s{"hello"};
// All six operators work for both orderings:
bool a = s < "world"; // MyString < string_view
bool b = "apple" < s; // string_view < MyString (synthesised)
bool c = s == std::string_view{"hello"};
bool d = std::string_view{"hello"} == s; // synthesised
}The compiler synthesises the reversed form by swapping operands and, if necessary, negating the result β no extra work required.
Choosing the right ordering category
Selecting the correct category is a semantic decision, not a performance one. The table below maps intent to type:
| Situation | Category | Reason |
|---|---|---|
| All values are fully distinguishable and totally ordered | strong_ordering | Equal implies substitutable |
| Values can be equivalent but still distinguishable | weak_ordering | Equal does not imply identical |
| Some pairs of values cannot be ordered at all | partial_ordering | unordered state must be representable |
Use weak_ordering for equivalence classes. A case-insensitive string is the canonical example: "abc" and "ABC" compare equivalent, but they are not the same string. Returning strong_ordering::equal would be a lie β it implies that the two values are interchangeable in all contexts, which they are not.
// C++20
#include <compare>
#include <cctype>
#include <string_view>
struct CIString {
std::string data;
// Returns weak_ordering because equivalent strings may differ in case
std::weak_ordering operator<=>(const CIString& rhs) const {
auto lbegin = data.begin(), lend = data.end();
auto rbegin = rhs.data.begin(), rend = rhs.data.end();
for (; lbegin != lend && rbegin != rend; ++lbegin, ++rbegin) {
unsigned char lc = std::tolower(static_cast<unsigned char>(*lbegin));
unsigned char rc = std::tolower(static_cast<unsigned char>(*rbegin));
if (auto cmp = lc <=> rc; cmp != 0)
return cmp;
}
return data.size() <=> rhs.data.size();
}
bool operator==(const CIString& rhs) const {
if (data.size() != rhs.data.size()) return false;
for (std::size_t i = 0; i < data.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(data[i])) !=
std::tolower(static_cast<unsigned char>(rhs.data[i])))
return false;
}
return true;
}
};Use partial_ordering when incomparability is legitimate. Floating-point NaN cannot be ordered relative to any number, including itself. Semver version ranges (^1.2, ~2.0) may genuinely be incomparable. Any type where "neither A < B nor B < A nor A == B" is a valid outcome requires partial_ordering.
std::compare_three_way and std::lexicographical_compare_three_way
std::compare_three_way is the function-object form of <=>. It can be stored, passed as a template argument, or used anywhere a comparator is expected β just as std::less<> is the transparent function-object form of <. Using it in algorithms keeps code generic: the return type flows from the operands, not from a hard-coded category.
// C++20
#include <compare>
#include <algorithm>
#include <vector>
#include <functional>
// compare_three_way as a comparator argument
auto cmp = std::compare_three_way{};
auto r = cmp(42, 100); // std::strong_ordering::less
auto s = cmp(3.14, 2.71); // std::partial_ordering::greater
// Sort a vector of pairs using compare_three_way
std::vector<std::pair<int,int>> pts = {{1,3},{1,2},{2,1}};
std::sort(pts.begin(), pts.end(), [](const auto& a, const auto& b){
return std::compare_three_way{}(a, b) < 0;
});
// Range-level three-way comparison β lives in <algorithm>
std::vector<int> a{3, 1, 4};
std::vector<int> b{3, 1, 5};
// C++20 β returns strong_ordering::less here
auto ord = std::lexicographical_compare_three_way(
a.begin(), a.end(),
b.begin(), b.end()
);
// Can also supply a custom element comparator
auto ord2 = std::lexicographical_compare_three_way(
a.begin(), a.end(),
b.begin(), b.end(),
std::compare_three_way{}
);std::lexicographical_compare_three_way is especially useful when implementing operator<=> for container-like types: call it on the element range and return the result directly rather than hand-rolling the loop.
std::strong_order, std::weak_order, and std::partial_order
These three named comparison functions let you explicitly request a specific ordering tier, regardless of what operator<=> on the type returns. They are particularly valuable for floating-point numbers, where operator<=> returns partial_ordering (because of NaN) but a total order is sometimes needed β for example when sorting a container that must have a deterministic result even if NaN values are present.
std::strong_order on floating-point implements IEEE 754 totalOrder, which places NaN values at well-defined positions: negative NaN before negative infinity, positive NaN after positive infinity. This is distinct from operator<=> which produces unordered for any comparison involving NaN.
// C++20
#include <compare>
#include <limits>
#include <cassert>
void float_ordering_demo() {
double nan = std::numeric_limits<double>::quiet_NaN();
double inf = std::numeric_limits<double>::infinity();
// operator<=> β partial ordering, NaN produces unordered
auto r1 = nan <=> 1.0;
assert(r1 == std::partial_ordering::unordered);
// std::strong_order β total order, NaN has a defined position
auto r2 = std::strong_order(nan, 1.0);
// r2 is std::strong_ordering::less or ::greater β never unordered
// (positive NaN sorts after +inf per IEEE 754 totalOrder)
assert(r2 == std::strong_ordering::greater);
// std::weak_order β useful for types with custom equivalence classes
auto r3 = std::weak_order(1.0, 2.0); // weak_ordering::less
// std::partial_order β explicit partial order, falls back to <=>
auto r4 = std::partial_order(nan, inf);
assert(r4 == std::partial_ordering::unordered);
}Use std::strong_order when you need to put floating-point values into a sorted container or use them as map keys and cannot exclude NaN from the domain. For all other types, these named functions simply delegate to operator<=> after converting to the requested category.
Floating-point and the unordered state
#include <compare>
#include <limits>
void classify(double a, double b) {
auto r = a <=> b; // std::partial_ordering β C++20
if (r == std::partial_ordering::unordered) {
// at least one operand is NaN
} else if (std::is_lt(r)) {
// a < b, both are numbers
}
}
classify(std::numeric_limits<double>::quiet_NaN(), 1.0); // unordered
classify(1.0, 2.0); // lessMixed-type arithmetic comparison
Wrapping a scalar in a class type does not lose heterogeneous comparison β define operator<=> against each type you want to support. The generated reversed operators handle the opposite operand order, and the operator== can be defined once to cover all same-type equality checks.
// C++20
#include <compare>
class Temperature {
public:
explicit constexpr Temperature(double c) : celsius_(c) {}
// Compare Temperature to Temperature
auto operator<=>(const Temperature&) const = default;
// Compare Temperature to raw double (degrees Celsius)
std::partial_ordering operator<=>(double c) const {
return celsius_ <=> c;
}
bool operator==(double c) const { return celsius_ == c; }
// Compare Temperature to int (implicit promotion to double is fine)
std::partial_ordering operator<=>(int c) const {
return celsius_ <=> static_cast<double>(c);
}
bool operator==(int c) const { return celsius_ == static_cast<double>(c); }
double value() const { return celsius_; }
private:
double celsius_;
};
void demo() {
Temperature boiling{100.0};
bool a = boiling > 37.0; // Temperature > double
bool b = 37.0 < boiling; // double < Temperature (synthesised)
bool c = boiling == 100; // Temperature == int
bool d = 100 == boiling; // int == Temperature (synthesised)
// Only <=> is defined between Temperature and double/int.
// operator== is defined separately because short-circuiting on
// a floating-point equality check is worthwhile here.
}When only operator<=> is defined (without a corresponding operator==), the compiler will synthesise == from it β but that synthesised == invokes <=> and checks whether the result == 0, which is slightly more expensive than a direct equality comparison. For arithmetic types, the difference is negligible; for complex types with expensive ordering, providing an explicit operator== is worth it.
Implementing weak ordering with std::tie
For structs with multiple fields, the idiomatic way to express lexicographic ordering is std::tie combined with <=>. The trick works because std::tie produces a std::tuple of references, and tuple's own operator<=> already performs lexicographic comparison across its elements β you get correct multi-field ordering without writing a single loop.
// C++20
#include <compare>
#include <string>
#include <tuple>
struct Employee {
std::string name;
int department_id;
int employee_id; // unique tiebreaker
// Primary sort: name; secondary: department_id; final tiebreak: employee_id
auto operator<=>(const Employee& rhs) const {
return std::tie(name, department_id, employee_id)
<=> std::tie(rhs.name, rhs.department_id, rhs.employee_id);
}
bool operator==(const Employee& rhs) const {
return std::tie(name, department_id, employee_id)
== std::tie(rhs.name, rhs.department_id, rhs.employee_id);
}
};The return type is deduced from the tuple's <=>, which propagates the weakest category among its element types. Here std::string's <=> returns strong_ordering and int's <=> also returns strong_ordering, so the overall result is strong_ordering. If any field returned weak_ordering or partial_ordering, the result would downgrade accordingly.
This pattern scales to any number of fields without rewriting the comparison logic. Adding a new field to the tiebreak is a one-line change to both std::tie calls.
Using <=> to satisfy sorted container requirements
#include <compare>
#include <map>
#include <cstdint>
struct Endpoint {
std::uint32_t ip;
std::uint16_t port;
auto operator<=>(const Endpoint&) const = default;
bool operator==(const Endpoint&) const = default;
};
// operator< is synthesised from <=>, so Endpoint works as a map key directly.
std::map<Endpoint, int> connections;Before C++20, this struct required a hand-written operator< or a custom comparator. The defaulted spaceship synthesises all six relational operators with no maintenance burden.
Range-level three-way comparison
#include <compare>
#include <algorithm> // std::lexicographical_compare_three_way
#include <vector>
std::vector<int> a{3, 1, 4};
std::vector<int> b{3, 1, 5};
// C++20 β in <algorithm>, not <compare>
auto ord = std::lexicographical_compare_three_way(
a.begin(), a.end(),
b.begin(), b.end()
); // std::strong_ordering::less
// std::compare_three_way is the function-object form of <=>
// C++20
auto cmp = std::compare_three_way{}(42, 100); // std::strong_ordering::lessThe if (a <=> b) pitfall
A comparison result is never zero-valued in the boolean sense β all three ordering types are class types, and converting them to bool is not defined. More importantly, the intent of if (a <=> b) is almost certainly wrong: the programmer probably wanted to test equality or check the ordering, but what was written simply discards the result of the comparison.
// C++20
#include <compare>
void bad_and_good(int a, int b) {
// WRONG β this does not compile in C++20 because ordering types
// are not contextually convertible to bool. Good compilers also
// warn or error on this in earlier drafts.
// if (a <=> b) { ... } // ERROR or at best meaningless
// CORRECT β test for equality
if ((a <=> b) == 0) { /* a == b */ }
// CLEAREST β just use ==
if (a == b) { /* a == b */ }
// CORRECT β test ordering
if ((a <=> b) < 0) { /* a < b */ }
// CORRECT β use a named helper
if (std::is_lt(a <=> b)) { /* a < b */ }
}if (a <=> b) does not compile for the ordering category types because they lack a contextual conversion to bool. This is intentional: the type system forces you to state what you are testing. If you find yourself writing if (a <=> b), replace it with if (a == b) for equality or if ((a <=> b) < 0) for ordering.
Best Practices
Default when you can. If a type's natural ordering is member-wise comparison in field order, = default gives all six operators for free with zero maintenance. Always pair it with an explicit defaulted operator==.
Write operator== separately for custom types. When you define a non-defaulted operator<=>, the compiler deliberately does not generate operator== β equality can often short-circuit on a size check before comparing content, so conflating the two would be a performance regression. The two must remain consistent.
Choose the weakest correct category. Never declare strong_ordering when the semantics are only weak_ordering. Generic algorithms and concepts rely on these categories to reason about substitutability. Lying upward breaks contracts.
Use std::tie for multi-field lexicographic ordering. return std::tie(a, b, c) <=> std::tie(rhs.a, rhs.b, rhs.c) is idiomatic, correct, and scales with the struct without rewriting comparison logic.
Use named helpers in templates. std::is_lt(r) works with any of the three ordering types. r < 0 also works due to implicit conversion to int, but the named form is more self-documenting and more resilient to refactoring.
Use std::strong_order for float keys. When you need floating-point values in a sorted container and cannot exclude NaN, std::strong_order gives a total order with NaN at defined positions. Plain <=> leaves NaN unordered and breaks sorted-container invariants.
Common Pitfalls
Assuming float gives strong ordering. double <=> double yields std::partial_ordering. Passing floating-point results to code that expects std::strong_ordering fails to compile when the category is threaded through constrained templates β which is the correct behaviour, not a bug to work around.
Defaulted <=> without a consistent ==. Defaulting operator<=> auto-generates a defaulted operator== that also compares member-wise. If you then add a hand-written operator== with different semantics, you get inconsistent equality and ordering β a subtle correctness bug that passes all tests that check only ordering or only equality.
Writing if (a <=> b). The ordering types are not contextually convertible to bool. The intent is almost always if (a == b) or if ((a <=> b) < 0). Modern compilers reject or warn on the bare form; do not suppress the diagnostic.
Pointer comparison UB. <=> on pointer types returns std::strong_ordering, but comparing pointers into unrelated allocations is still implementation-defined, exactly as with <. The type system cannot protect you here.
C++17 build targets. The <compare> header, the <=> operator, and all ordering types are absent before C++20. Conditionally enabling them behind __cpp_lib_three_way_comparison without also guarding all six comparison operators for the fallback path leaves the type unusable as a map key in C++17 builds.
Non-defaulted <=> does not generate ==. This surprises developers who expect all six operators from any operator<=> definition. Only a defaulted operator<=> triggers the automatic generation of a defaulted operator==. Hand-written implementations must always provide an explicit operator==.
See Also
std::three_way_comparable,std::totally_orderedβ C++20 concepts constraining types by ordering supportstd::lexicographical_compare_three_wayβ range-level three-way comparison in<algorithm>std::compare_three_wayβ function object equivalent of<=>, usable as a comparator template argumentreference/library/utilities/chronoβstd::chronoduration and time_point provide<=>since C++20