C++ / OPERATORS AND EXPRESSIONS
Comparisons and three-way comparison with <=>
Use C++20's <=> to get less/equal/greater from one comparison, pick the right ordering category, and default it so all six operators exist.
What you will learn
- Read a <=> b result by comparing it against the literal 0, not an int variable
- Choose strong_ordering, weak_ordering or partial_ordering by what equal must promise
- Default operator<=> to get all six comparisons in member declaration order
- Add operator== by hand whenever you write operator<=> by hand
Understanding Comparisons and three-way comparison with <=>
`<`, `>`, `<=`, `>=`, `==` and `!=` each answer one yes/no question, so a routine that needs to know whether two values are below, equal to, or above each other has to ask twice. C++20's `a <=> b` answers all three in one call: it returns an object whose value is `less`, `equal` or `equivalent`, `greater`, or in one case `unordered`. You never read that object as a number, and `int r = a <=> b;` will not compile; you interrogate it by comparing it against the literal `0`, so `(a <=> b) < 0` means "a comes first". The literal is required rather than an `int` variable holding zero, because the comparison against zero is declared through a type that only a literal `0` can convert to.
The return type is not always the same, and that is the interesting part: `std::strong_ordering`, `std::weak_ordering` and `std::partial_ordering` differ in what their neither-less-nor-greater case promises. `strong_ordering::equal` says the two objects are substitutable, with nothing observable distinguishing them, while `weak_ordering::equivalent` only says they occupy the same slot in the order, which is what case-insensitive text or magnitude ordering needs. `partial_ordering` adds `unordered` for pairs that stand in no relation at all, which is why `1.0 <=> nan` has that type: with IEEE floating point, `<`, `>` and `==` are all false at once for a NaN. Picking the weakest category you can honestly promise is a message to every future caller about what they may conclude from equality.
For your own types you rarely write six operators. Writing `auto operator<=>(const T&) const = default;` compares members top to bottom in declaration order and stops at the first difference, so member order is sort-key order: a `Date` declared `year, month, day` sorts chronologically and the same struct declared `day, month, year` does not. A defaulted `<=>` also implicitly declares a defaulted `operator==`, and the compiler rewrites `a > b` into `(a <=> b) > 0` and `a != b` into `!(a == b)`. The asymmetry to remember is that equality is never routed through `<=>`, so a hand-written `operator<=>` gives you four relational operators and no equality until you add `operator==` yourself; the split exists because equality is often far cheaper to decide, the way two vectors of different length are unequal without inspecting a single element.
placeholder
<compare>
<iostream>
struct Date {
int year;
int month;
int day;
auto operator<=>(const Date&) const = default;
};
int main() {
Date launch{2020, 3, 15};
Date review{2020, 11, 2};
std::cout << std::boolalpha;
std::cout << "launch < review : " << (launch < review) << '\n';
std::cout << "launch == review: " << (launch == review) << '\n';
std::cout << "launch >= launch: " << (launch >= launch) << '\n';
std::strong_ordering c = launch <=> review;
std::cout << "c < 0 : " << (c < 0) << '\n';
std::cout << "c == 0 : " << (c == 0) << '\n';
std::cout << "is_lt(c) : " << std::is_lt(c) << '\n';
}
One `<=>` call returns a comparison category object rather than a bool, and the category you return tells callers whether equal means substitutable, merely equivalent, or possibly unordered.
Worked examples
Hand-written ordering with a reversed key
Orders players by score descending and then name ascending, showing that a user-written <=> must be paired with its own operator==.
<algorithm>
<compare>
<iostream>
<string>
<vector>
struct Player {
std::string name;
int score;
std::strong_ordering operator<=>(const Player& other) const {
if (auto c = other.score <=> score; c != 0) return c;
return name <=> other.name;
}
bool operator==(const Player& other) const = default;
};
int main() {
std::vector<Player> v{{"ada", 30}, {"bob", 50}, {"cy", 30}};
std::sort(v.begin(), v.end());
for (const Player& p : v)
std::cout << p.name << ' ' << p.score << '\n';
std::cout << std::boolalpha << "ada < cy: " << (v[1] < v[2]) << '\n';
}
Example explained
Line 1`other.score <=> score` puts the operands in reverse order, which flips that key so the highest score sorts first.
Line 2The `if (auto c = ...; c != 0)` init-statement returns early only when the score key already decided the order, otherwise the name key breaks the tie.
Line 3`bool operator==(const Player&) const = default;` is mandatory here: writing `<=>` yourself declares no equality, so without this line `v[1] == v[2]` would not compile.
Line 4`std::sort` never calls `<=>` directly; it calls `<`, which the compiler rewrote into `(a <=> b) < 0`.
weak_ordering for equivalent but unequal values
Ranks offsets by magnitude, so -5 and 5 are equivalent in the order while remaining observably different objects.
<compare>
<iostream>
struct Offset {
int value;
std::weak_ordering operator<=>(const Offset& other) const {
int a = value < 0 ? -value : value;
int b = other.value < 0 ? -other.value : other.value;
return a <=> b;
}
bool operator==(const Offset& other) const {
return (*this <=> other) == 0;
}
};
int main() {
Offset p{5}, n{-5}, big{9};
std::cout << std::boolalpha;
std::cout << "p < big : " << (p < big) << '\n';
std::cout << "p == n : " << (p == n) << '\n';
std::cout << "values equal: " << (p.value == n.value) << '\n';
std::cout << "p < n : " << (p < n) << '\n';
std::cout << "n < p : " << (n < p) << '\n';
}
Example explained
Line 1Declaring the return type `std::weak_ordering` states up front that two distinguishable objects may compare equivalent, which is true for -5 and 5 here.
Line 2`a <=> b` on two ints produces `std::strong_ordering`, which converts implicitly to the weaker `std::weak_ordering` return type; the reverse conversion does not exist.
Line 3`p == n` is true while `p.value == n.value` is false, so equality here means "same position in the order", not "same object".
Line 4`p < n` and `n < p` are both false, which is what equivalence looks like from the caller's side.
partial_ordering and NaN
Shows that comparing a double with NaN yields unordered, where less, greater and equal are all false simultaneously.
<compare>
<iostream>
<limits>
int main() {
double qnan = std::numeric_limits<double>::quiet_NaN();
std::cout << std::boolalpha;
std::partial_ordering c = 1.0 <=> qnan;
std::cout << "c == unordered: " << (c == std::partial_ordering::unordered) << '\n';
std::cout << "c < 0 : " << (c < 0) << '\n';
std::cout << "c > 0 : " << (c > 0) << '\n';
std::cout << "c == 0 : " << (c == 0) << '\n';
std::cout << "1.0 < qnan : " << (1.0 < qnan) << '\n';
std::partial_ordering d = 1.0 <=> 2.0;
std::cout << "d < 0 : " << (d < 0) << '\n';
std::cout << "d unordered : " << (d == std::partial_ordering::unordered) << '\n';
}
Example explained
Line 1`1.0 <=> qnan` has type `std::partial_ordering` because IEEE doubles contain values that sit outside any ordering.
Line 2`c < 0`, `c > 0` and `c == 0` are false at the same time, so code that tests only `c < 0` and treats the else branch as "greater or equal" quietly mishandles NaN.
Line 3`c == std::partial_ordering::unordered` is the only way to detect that fourth case, and no such value exists in `weak_ordering` or `strong_ordering`.
Line 4`1.0 <=> 2.0` is also `partial_ordering` even though it yields `less`, because the category comes from the operand types, not from the particular values.
Important notes
`<=>` and the rewriting of `<`, `>`, `<=`, `>=` need C++20: build with `-std=c++20` on gcc or clang, `/std:c++20` on MSVC.
Unlike `<`, the built-in `<=>` refuses mixed signed and unsigned operands: `-1 < 1u` compiles and is false because `-1` converts to a huge unsigned value, whereas `-1 <=> 1u` is a compile error.
Common mistakes
Writing `operator<=>` by hand and expecting equality to come along: only a defaulted `<=>` implicitly declares a defaulted `operator==`, so `a == b` and `a != b` fail to compile until you declare `operator==` yourself.
Treating the result as a number. `int r = a <=> b;` does not compile, and neither does `(a <=> b) < zero` where `zero` is an `int` variable, since those comparisons are only declared against a literal `0`.
Returning `std::strong_ordering` from a comparison that ignores part of the state, such as a case-insensitive or absolute-value ordering. It compiles, but you have promised that equal objects are substitutable, and callers who cache or de-duplicate on equality will then drop values that differ.
Try it yourself
Change, predict, then run
Write a `Duration` struct with `int hours; int minutes;` and a defaulted `operator<=>`, then print whether `Duration{1, 90} < Duration{2, 0}`. Swap the two member declarations, rerun, and explain why the answer flips.
Open the C++ workspaceCheck your understanding
A `Person` type has a hand-written `operator<=>` returning `std::weak_ordering` that compares only the `id` member, and no `operator==`. What happens to `p1 == p2` and `p1 < p2`?
- Both compile; `p1 == p2` is rewritten as `(p1 <=> p2) == 0`.
- `p1 < p2` compiles, but `p1 == p2` does not: only a defaulted `operator<=>` also declares `operator==`.
- Neither compiles, because a `weak_ordering` result cannot be compared against `0`.
- Both compile, but `p1 == p2` compares every member while `p1 < p2` compares only `id`.
Show answer
`p1 < p2` is rewritten to `(p1 <=> p2) < 0`, so the relational operators all work. Equality is never rewritten through `<=>`; the compiler only synthesizes `operator==` when `<=>` is defaulted, so here `==` is simply undeclared and the expression is an error. Option 0 is tempting because `<` really is rewritten that way, but the language keeps equality separate so types like `std::vector` can answer `==` with a cheap size check instead of an ordering walk.