Skip to content
C++
Language
since C++20
Basic

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++20

operator<=> 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

cpp
#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.

cpp
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.

cpp
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 strings

std::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.

cpp
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 hold

For 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.

cpp
// 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:

cpp
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

cpp
#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.

cpp
#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 rewrite

Named predicates and std::compare_three_way (C++20)

cpp
#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_ordering

lexicographical_compare_three_way (C++20)

cpp
#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

cpp
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

cpp
struct Record {
    int         id;
    std::string name;

    auto operator<=>(const Record&) const = default;
    // All 6 operators available; operator== also synthesised
};

Migration checklist:

  1. Replace all six relational operators with auto operator<=>(const T&) const = default;.
  2. Remove the explicit operator== — it is synthesised automatically when <=> is defaulted.
  3. If float/double members exist but you only care about strong ordering for specific fields, write a custom <=> with explicit std::strong_ordering return type and add operator== manually.
  4. 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 noexcept on custom bodies: the compiler cannot infer these for non-defaulted implementations.
  • Use named predicates in generic code: std::is_lt(r) reads better than r < 0 and 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 like is_subset_of() rather than <=>, to avoid confusing callers who expect total ordering semantics from standard comparison operators.

Common Pitfalls

cpp
// 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