AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

11 Operator overloading / Lesson 2 of 5

Comparison and the spaceship operator

15 min · +10 XP · 2 exercises · 7 questions

Read the text15 min
Exercises0/2
Quiz7 q

Quick answer

What is the spaceship operator in C++?

The spaceship operator <=> is C++20's three-way comparison: a <=> b answers less, equal or greater in one call and returns a comparison category such as std::strong_ordering. The compiler rewrites a < b as (a <=> b) < 0. Declaring auto operator<=>(const T&) const = default; compares the members in declaration order and also defaults ==, so one line gives all six comparison operators.

A type that can be compared can be sorted, searched, deduplicated and used as a std::map key — most of the standard library opens up the moment < and == exist. Before C++20 that meant writing six operators by hand and keeping them consistent; since C++20 one line, auto operator<=>(const T&) const = default;, produces the whole set from the members in declaration order. This lesson settles what == and < must promise, how the three-way comparison <=> and the rewritten expressions work, which of strong_ordering, weak_ordering and partial_ordering a type should return, and what std::sort and std::set actually call.

Equality

struct Version {
    int major, minor, patch;
    bool operator==(const Version&) const = default;   // memberwise, in declaration order
};

Version a{1, 2, 3}, b{1, 2, 4};
a == b;   // false
a != b;   // true — rewritten by the compiler as !(a == b)

operator== returns bool and is a const member (or a free function with two const& parameters). Defaulting it compares each member with its own ==, in the order they are declared, and stops at the first difference. C++20 also rewrites: a != b becomes !(a == b), and when the operand types differ, b == a is tried as a == b reversed — so one == serves both directions and != no longer needs writing. Equality must be an equivalence relation: reflexive, symmetric, transitive. A hand-written == that ignores a member (a cached hash, a timestamp) is legitimate as long as < ignores it too.

Ordering before C++20

Ordered containers and std::sort need < and nothing else, and < must be a strict weak ordering: a < a is false, a < b implies !(b < a), and "neither is less" is transitive. The pre-C++20 way to write it for several members is std::tie, which compares lexicographically:

bool operator<(const Version& a, const Version& b) {
    return std::tie(a.major, a.minor, a.patch) < std::tie(b.major, b.minor, b.patch);
}
bool operator>(const Version& a, const Version& b)  { return b < a; }
bool operator<=(const Version& a, const Version& b) { return !(b < a); }
bool operator>=(const Version& a, const Version& b) { return !(a < b); }

Four functions derived from one, and ==/!= separately: six operators for one idea. The classic bug is <= written as the comparison itself (return a.x <= b.x;) — not strict, so std::sort may read out of bounds and std::set cannot find what it just inserted.

The spaceship operator

a <=> b asks "less, equal or greater?" once and answers with a comparison category value that itself compares to 0:

#include <compare>

struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;   // also declares operator== as defaulted
};

Version a{1, 2, 10}, b{1, 2, 9};
a < b;         // rewritten as (a <=> b) < 0    → false
a >= b;        // rewritten as (a <=> b) >= 0   → true
a == b;        // the defaulted ==, NOT <=>     → false
std::strong_ordering r = a <=> b;   // std::strong_ordering::greater

The compiler rewrites each of <, >, <= and >= as a call to <=> compared with the literal 0 (only 0 — (a <=> b) < 1 is an error by design). A defaulted <=> compares members in declaration order and, as a courtesy, also declares a defaulted operator==, so one line yields all six. The order of the members is therefore the order of comparison: major, minor, patch sorts 1.2.9 before 1.2.10, which a string comparison would not.

== is deliberately not derived from <=>. For a std::string the three-way comparison must scan the characters, while equality can reject two strings of different length instantly, so the language keeps them separate — and that has a consequence: if you write <=> yourself, you get the four relational operators and no ==. Add bool operator==(const T&) const = default; (or your own) alongside, or a == b does not compile.

Three orderings

CategoryMeaningExamplesConverts to
std::strong_orderingEqual values are substitutableint, std::string, a defaulted Versionweak, partial
std::weak_orderingEquivalent but distinguishableCase-insensitive names, a struct with a member <=> ignorespartial
std::partial_orderingSome pairs are unordereddouble (NaN), a struct with a double member—

auto as the return type of a defaulted <=> deduces the weakest category among the members, so adding one double field turns a strong_ordering type into a partial_ordering one — and a < b becomes false in both directions for a NaN. Declaring the return type as std::strong_ordering instead makes the defaulted operator deleted when a member cannot supply that category, so the first comparison fails to compile — which is the check you want. Stronger categories convert implicitly to weaker ones: a function returning std::weak_ordering may return x.size() <=> y.size(); even though that expression is strong.

Writing <=> by hand

Default when the members in declaration order are the ordering. Otherwise compose it: compare the most significant key, return if decided, fall through to the next:

struct Date {
    int y, m, d;
    std::strong_ordering operator<=>(const Date& o) const {
        if (auto c = y <=> o.y; c != 0) return c;
        if (auto c = m <=> o.m; c != 0) return c;
        return d <=> o.d;
    }
    bool operator==(const Date&) const = default;   // must be declared: a user-provided <=> gives no ==
};

A case-insensitive Tag compares lower-cased characters and returns std::weak_ordering, because "Apple" and "apple" are equivalent — neither is less — yet remain distinguishable objects; its == must agree, and (*this <=> o) == 0 is the simplest way to guarantee that. Keep the two consistent: a std::set that finds an element by < and a std::find that finds it by == must agree about which elements are the same.

Sort, set and map

std::vector<Version> v = {{1, 2, 10}, {1, 2, 9}, {2, 0, 0}};
std::sort(v.begin(), v.end());        // uses operator<  → (v[i] <=> v[j]) < 0
std::set<Version> distinct(v.begin(), v.end());
std::map<Version, std::string> notes; // keys ordered by the same <
distinct.contains(Version{1, 2, 9});  // found by "neither is less", not by ==

Every ordered facility in the library — std::sort, std::set, std::map, std::lower_bound, std::min — is written in terms of < alone, through the default comparator std::less<T>. A defaulted <=> supplies that < and nothing more is needed. Two keys are the same key for a std::set when !(a < b) && !(b < a), so a Tag with a case-insensitive ordering deduplicates spellings that differ only in case, keeping the one inserted first. When the natural order is not the one a particular container needs, pass a comparator instead of changing < (lesson 4).

Pitfalls

MistakeWhat happens
<= or >= used as the sort orderNot strict: std::sort is undefined behaviour, std::set loses elements
Hand-written <=>, no ==a == b and a != b do not compile
Members declared in the wrong order, <=> defaultedSorts by the wrong key first
double member, auto returnpartial_ordering; NaN compares neither less nor greater
(a <=> b) < 1Compile error: an ordering compares only with the literal 0
Forgetting #include <compare>The category types are undeclared
== and < that disagreestd::set and std::find see different duplicates

Key takeaways

  • operator== is enough for != (rewritten) and, when defaulted, compares members in declaration order.
  • auto operator<=>(const T&) const = default; gives <, >, <=, >= and ==; a hand-written <=> gives the four relations only — add == yourself.
  • The result compares with 0: a < b is (a <=> b) < 0.
  • strong_ordering for substitutable values, weak_ordering for equivalent-but-distinguishable, partial_ordering when some pairs are unordered (double).
  • std::sort, std::set and std::map use only <; a strict weak ordering is the contract, and "same key" means neither is less.
  • Compose a custom <=> key by key with if (auto c = x <=> o.x; c != 0) return c;.

Common questions

Why does a hand-written operator<=> not generate operator==?

Only a defaulted <=> also declares a defaulted ==. Equality is kept separate because it can often be answered faster — two strings of different length are unequal without comparing any characters. If you write <=> yourself you get <, >, <= and >=, and must add bool operator==(const T&) const = default; or a == b does not compile.

What is the difference between strong_ordering, weak_ordering and partial_ordering?

std::strong_ordering means equal values are interchangeable, as with int and std::string. std::weak_ordering means equivalent values can still be told apart, such as case-insensitive names. std::partial_ordering allows pairs that are unordered, as double does with NaN. A defaulted <=> returning auto deduces the weakest category among the members.

What is a strict weak ordering in C++?

It is the contract std::sort, std::set and std::map require of < or a comparator: a < a is false, a < b implies !(b < a), and both "less" and "neither is less" are transitive. A comparison written with <= breaks it — undefined behaviour in std::sort, lost elements in a std::set.

How do I compare a struct by several fields in C++?

Since C++20, declare auto operator<=>(const T&) const = default; and the members are compared lexicographically in declaration order, so declare them in order of significance. Before C++20, write operator< as std::tie(a.x, a.y) < std::tie(b.x, b.y), or compose a custom <=> key by key, returning at the first result that is not zero.

Does std::set use operator== to find duplicates?

No. std::set and std::map use only <, through std::less: two keys count as the same when neither is less than the other. That is why == and < must agree — when they disagree, a std::set and std::find see different duplicates.

Mark as readSign in to keep your place.

C++20 Spaceship Operator: Three-Way Comparison Explained

The C++20 spaceship operator compares once and answers less, equal or greater. Defaulting it gives all six comparisons; strong, weak and partial ordering.

  • Course: C++ study plan
  • Module: Operator overloading
  • Kind: Lesson
  • Reading time: 15 min
  • Runtime: Clang 18 · C++20

What is the spaceship operator in C++?

The spaceship operator <=> is C++20's three-way comparison: a <=> b answers less, equal or greater in one call and returns a comparison category such as std::strong_ordering. The compiler rewrites a < b as (a <=> b) < 0. Declaring auto operator<=>(const T&) const = default; compares the members in declaration order and also defaults ==, so one line gives all six comparison operators.

Lesson

A type that can be compared can be sorted, searched, deduplicated and used as a std::map key — most of the standard library opens up the moment < and == exist. Before C++20 that meant writing six operators by hand and keeping them consistent; since C++20 one line, auto operator<=>(const T&) const = default;, produces the whole set from the members in declaration order. This lesson settles what == and < must promise, how the three-way comparison <=> and the rewritten expressions work, which of strong_ordering, weak_ordering and partial_ordering a type should return, and what std::sort and std::set actually call.

Equality

struct Version {
    int major, minor, patch;
    bool operator==(const Version&) const = default;   // memberwise, in declaration order
};

Version a{1, 2, 3}, b{1, 2, 4};
a == b;   // false
a != b;   // true — rewritten by the compiler as !(a == b)

operator== returns bool and is a const member (or a free function with two const& parameters). Defaulting it compares each member with its own ==, in the order they are declared, and stops at the first difference. C++20 also rewrites: a != b becomes !(a == b), and when the operand types differ, b == a is tried as a == b reversed — so one == serves both directions and != no longer needs writing. Equality must be an equivalence relation: reflexive, symmetric, transitive. A hand-written == that ignores a member (a cached hash, a timestamp) is legitimate as long as < ignores it too.

Ordering before C++20

Ordered containers and std::sort need < and nothing else, and < must be a strict weak ordering: a < a is false, a < b implies !(b < a), and "neither is less" is transitive. The pre-C++20 way to write it for several members is std::tie, which compares lexicographically:

bool operator<(const Version& a, const Version& b) {
    return std::tie(a.major, a.minor, a.patch) < std::tie(b.major, b.minor, b.patch);
}
bool operator>(const Version& a, const Version& b)  { return b < a; }
bool operator<=(const Version& a, const Version& b) { return !(b < a); }
bool operator>=(const Version& a, const Version& b) { return !(a < b); }

Four functions derived from one, and ==/!= separately: six operators for one idea. The classic bug is <= written as the comparison itself (return a.x <= b.x;) — not strict, so std::sort may read out of bounds and std::set cannot find what it just inserted.

The spaceship operator

a <=> b asks "less, equal or greater?" once and answers with a comparison category value that itself compares to 0:

#include <compare>

struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;   // also declares operator== as defaulted
};

Version a{1, 2, 10}, b{1, 2, 9};
a < b;         // rewritten as (a <=> b) < 0    → false
a >= b;        // rewritten as (a <=> b) >= 0   → true
a == b;        // the defaulted ==, NOT <=>     → false
std::strong_ordering r = a <=> b;   // std::strong_ordering::greater

The compiler rewrites each of <, >, <= and >= as a call to <=> compared with the literal 0 (only 0 — (a <=> b) < 1 is an error by design). A defaulted <=> compares members in declaration order and, as a courtesy, also declares a defaulted operator==, so one line yields all six. The order of the members is therefore the order of comparison: major, minor, patch sorts 1.2.9 before 1.2.10, which a string comparison would not.

== is deliberately not derived from <=>. For a std::string the three-way comparison must scan the characters, while equality can reject two strings of different length instantly, so the language keeps them separate — and that has a consequence: if you write <=> yourself, you get the four relational operators and no ==. Add bool operator==(const T&) const = default; (or your own) alongside, or a == b does not compile.

Three orderings

CategoryMeaningExamplesConverts to
std::strong_orderingEqual values are substitutableint, std::string, a defaulted Versionweak, partial
std::weak_orderingEquivalent but distinguishableCase-insensitive names, a struct with a member <=> ignorespartial
std::partial_orderingSome pairs are unordereddouble (NaN), a struct with a double member—

auto as the return type of a defaulted <=> deduces the weakest category among the members, so adding one double field turns a strong_ordering type into a partial_ordering one — and a < b becomes false in both directions for a NaN. Declaring the return type as std::strong_ordering instead makes the defaulted operator deleted when a member cannot supply that category, so the first comparison fails to compile — which is the check you want. Stronger categories convert implicitly to weaker ones: a function returning std::weak_ordering may return x.size() <=> y.size(); even though that expression is strong.

Writing <=> by hand

Default when the members in declaration order are the ordering. Otherwise compose it: compare the most significant key, return if decided, fall through to the next:

struct Date {
    int y, m, d;
    std::strong_ordering operator<=>(const Date& o) const {
        if (auto c = y <=> o.y; c != 0) return c;
        if (auto c = m <=> o.m; c != 0) return c;
        return d <=> o.d;
    }
    bool operator==(const Date&) const = default;   // must be declared: a user-provided <=> gives no ==
};

A case-insensitive Tag compares lower-cased characters and returns std::weak_ordering, because "Apple" and "apple" are equivalent — neither is less — yet remain distinguishable objects; its == must agree, and (*this <=> o) == 0 is the simplest way to guarantee that. Keep the two consistent: a std::set that finds an element by < and a std::find that finds it by == must agree about which elements are the same.

Sort, set and map

std::vector<Version> v = {{1, 2, 10}, {1, 2, 9}, {2, 0, 0}};
std::sort(v.begin(), v.end());        // uses operator<  → (v[i] <=> v[j]) < 0
std::set<Version> distinct(v.begin(), v.end());
std::map<Version, std::string> notes; // keys ordered by the same <
distinct.contains(Version{1, 2, 9});  // found by "neither is less", not by ==

Every ordered facility in the library — std::sort, std::set, std::map, std::lower_bound, std::min — is written in terms of < alone, through the default comparator std::less<T>. A defaulted <=> supplies that < and nothing more is needed. Two keys are the same key for a std::set when !(a < b) && !(b < a), so a Tag with a case-insensitive ordering deduplicates spellings that differ only in case, keeping the one inserted first. When the natural order is not the one a particular container needs, pass a comparator instead of changing < (lesson 4).

Pitfalls

MistakeWhat happens
<= or >= used as the sort orderNot strict: std::sort is undefined behaviour, std::set loses elements
Hand-written <=>, no ==a == b and a != b do not compile
Members declared in the wrong order, <=> defaultedSorts by the wrong key first
double member, auto returnpartial_ordering; NaN compares neither less nor greater
(a <=> b) < 1Compile error: an ordering compares only with the literal 0
Forgetting #include <compare>The category types are undeclared
== and < that disagreestd::set and std::find see different duplicates

Key takeaways

  • operator== is enough for != (rewritten) and, when defaulted, compares members in declaration order.
  • auto operator<=>(const T&) const = default; gives <, >, <=, >= and ==; a hand-written <=> gives the four relations only — add == yourself.
  • The result compares with 0: a < b is (a <=> b) < 0.
  • strong_ordering for substitutable values, weak_ordering for equivalent-but-distinguishable, partial_ordering when some pairs are unordered (double).
  • std::sort, std::set and std::map use only <; a strict weak ordering is the contract, and "same key" means neither is less.
  • Compose a custom <=> key by key with if (auto c = x <=> o.x; c != 0) return c;.

Common questions

Why does a hand-written operator<=> not generate operator==?

Only a defaulted <=> also declares a defaulted ==. Equality is kept separate because it can often be answered faster — two strings of different length are unequal without comparing any characters. If you write <=> yourself you get <, >, <= and >=, and must add bool operator==(const T&) const = default; or a == b does not compile.

What is the difference between strong_ordering, weak_ordering and partial_ordering?

std::strong_ordering means equal values are interchangeable, as with int and std::string. std::weak_ordering means equivalent values can still be told apart, such as case-insensitive names. std::partial_ordering allows pairs that are unordered, as double does with NaN. A defaulted <=> returning auto deduces the weakest category among the members.

What is a strict weak ordering in C++?

It is the contract std::sort, std::set and std::map require of < or a comparator: a < a is false, a < b implies !(b < a), and both "less" and "neither is less" are transitive. A comparison written with <= breaks it — undefined behaviour in std::sort, lost elements in a std::set.

How do I compare a struct by several fields in C++?

Since C++20, declare auto operator<=>(const T&) const = default; and the members are compared lexicographically in declaration order, so declare them in order of significance. Before C++20, write operator< as std::tie(a.x, a.y) < std::tie(b.x, b.y), or compose a custom <=> key by key, returning at the first result that is not zero.

Does std::set use operator== to find duplicates?

No. std::set and std::map use only <, through std::less: two keys count as the same when neither is less than the other. That is why == and < must agree — when they disagree, a std::set and std::find see different duplicates.

Exercises

Sort the versions

Write a Version struct with major, minor and patch members in that order and a defaulted three-way comparison: auto operator<=>(const Version&) const = default; — nothing else. Read n versions, sort them ascending with std::sort, print them, then put them in a std::set<Version> and report how many distinct versions there are and the oldest and newest.

Numeric order, not text order: 1.2.9 comes before 1.2.10, and 1.10.0 before 2.0.0.

Input: n (at least 1), then n lines each major.minor.patch. Output: the n versions sorted, one per line; then distinct=<k>, oldest=<version>, newest=<version>.

5
1.2.10
1.2.9
2.0.0
1.10.0
1.2.9

prints

1.2.9
1.2.9
1.2.10
1.10.0
2.0.0
distinct=4
oldest=1.2.9
newest=2.0.0

Case-insensitive tags

Write a Tag class wrapping a std::string whose ordering ignores case: "Apple", "apple" and "APPLE" are equivalent — neither is less than the other — but they remain different strings, so the three-way comparison must return std::weak_ordering, not std::strong_ordering. Write operator<=> by hand (compare the lower-cased characters one by one, then the lengths) and an operator== that agrees with it.

Read n tags into a std::set<Tag>: a spelling equivalent to one already present is not inserted, so the first spelling seen is the one kept. Print how many distinct tags there are and the tags in set order. Then read two more words and print how the first compares to the second.

Input: n, then n words (one per line), then a line with two words. Output: distinct=<k>, the k tags in order, then less, greater or equivalent.

5
Apple
banana
APPLE
Cherry
Banana
apple Apple

prints

distinct=3
Apple
banana
Cherry
equivalent

In this module: Operator overloading

← Overloading rules — members, free functions and the compound-first idiom · Stream operators — printing and reading your own types →