Skip to content
C++
Library
since C++20
Intermediate

<compare>

Three-way comparison ordering categories, the &lt;=> spaceship operator, and named comparison helpers introduced in C++20.

<compare>since C++20

The <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

TypeSemanticsTypical Source
std::strong_orderingTotal order; equal values are substitutableIntegers, enumerations
std::weak_orderingTotal order; equivalent values may differ in identityCase-insensitive strings
std::partial_orderingPartial order; some pairs may be unorderedFloating-point (NaN)

Each type exposes static member constants:

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

The 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:

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

Prefer these over comparing against ::less or against 0 when writing templates that must handle all three category types.

Syntax

cpp
// 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 spaceship generates all six operators

cpp
#include <compare>
#include <string>

struct Version {
    int major;
    int minor;
    int patch;
    std::string label;

    // C++20: member-wise comparison in declaration order.
    // Defaulted <=> also generates a defaulted operator==.
    auto operator<=>(const Version&) const = default;
};

int main() {
    Version v1{1, 2, 0, "alpha"};
    Version v2{1, 3, 0, ""};

    bool older  = v1 < v2;   // true: minor 2 < 3
    bool differ = v1 != v2;  // true β€” synthesised from operator==
}

= default makes the compiler emit member-wise comparison, short-circuiting at the first non-equal field. Because the operator is defaulted (not merely defined), the compiler also synthesises a corresponding operator==.

Custom operator<=> with explicit weak ordering

cpp
#include <compare>
#include <cctype>
#include <string>

struct CIString {
    std::string data;

    std::weak_ordering operator<=>(const CIString& rhs) const {
        std::size_t n = std::min(data.size(), rhs.data.size());
        for (std::size_t i = 0; i < n; ++i) {
            unsigned char a = std::tolower(static_cast<unsigned char>(data[i]));
            unsigned char b = std::tolower(static_cast<unsigned char>(rhs.data[i]));
            if (auto cmp = a <=> b; cmp != 0)  // C++17 init-statement
                return cmp;
        }
        return data.size() <=> rhs.data.size();
    }

    // C++20: compiler does NOT synthesise == from a non-defaulted <=>.
    // Must be defined explicitly, and must be consistent with <=>.
    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;
    }
};

"abc" and "ABC" are equivalent under this ordering but are not identical, which is why weak_ordering is the correct category β€” equivalent does not imply substitutability.

Floating-point and the unordered state

cpp
#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);                                        // less

Using <=> to satisfy sorted container requirements

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

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

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

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.

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.

See Also

  • std::three_way_comparable, std::totally_ordered β€” C++20 concepts constraining types by ordering support
  • std::lexicographical_compare_three_way β€” range-level three-way comparison in <algorithm>
  • std::compare_three_way β€” function object equivalent of <=>, usable as a comparator template argument
  • reference/library/utilities/chrono β€” std::chrono duration and time_point provide <=> since C++20