Three-Way Comparison (C++20)
Before C++20, making a class fully comparable required writing up to 6 operator overloads (==, !=, <, >, <=, >=) — tedious and error-prone. The three-way comparison operator (<=>), informally "the spaceship", returns an ordering in one operation, and when defaulted, the compiler auto-generates all six relational operators for you.
Use defaulted <=> as your first choice for any class that needs comparison. Write a custom <=> only when member-wise comparison is wrong (e.g., case-insensitive strings, semantic versioning).
Prerequisites: See 02_oop/classes/ first.
1. Defaulted <=> — one line gives you all 6 operators
The compiler compares members in declaration order, just like it does for defaulted constructors. If you write a custom <=>, you must explicitly default or define == yourself — it is NOT synthesized from <=>.
Watch out: defaulted <=> also generates a defaulted ==.
struct Point {
int x, y;
// This one line gives us ==, !=, <, >, <=, >= !
auto operator<=>(const Point&) const = default;
};2. Comparison categories
The return type of <=> tells you about the ordering:
strong_ordering: exactly one of <, ==, > is true.
Equal values are indistinguishable (e.g., int).
weak_ordering: equivalent values may not be identical.
(e.g., case-insensitive string comparison).
partial_ordering: some values may be unordered (e.g., double
with NaN — NaN is not less than, equal to,
or greater than anything, including itself).
the defaulted <=> returns partial_ordering for the whole class. Watch out: if any member has partial_ordering (like double),
3. Custom <=> implementation
Write your own when member-wise order is wrong. Return the appropriate ordering category. define or default operator== separately. The compiler will not synthesize == from your custom <=>.
Watch out: when you define a custom <=>, you MUST also
class Version {
int major_, minor_, patch_;
public:
Version(int major, int minor, int patch)
: major_(major), minor_(minor), patch_(patch) {}
// Custom three-way comparison: compare major first, then minor, then patch
std::strong_ordering operator<=>(const Version& other) const {
if (auto cmp = major_ <=> other.major_; cmp != 0) return cmp;
if (auto cmp = minor_ <=> other.minor_; cmp != 0) return cmp;
return patch_ <=> other.patch_;
}
// Reason: custom <=> does not generate ==; must be explicit
bool operator==(const Version&) const = default;
std::string to_string() const {
return std::format("{}.{}.{}", major_, minor_, patch_);
}
};4. Partial ordering with floating point
double's <=> returns std::partial_ordering because NaN is unordered relative to every value, including itself.
void partial_ordering_demo() {
std::cout << "\n--- Partial Ordering (double / NaN) ---\n";
double a = 1.0, b = 2.0, nan = std::nan("");
auto result = a <=> b;
if (result < 0) {
std::cout << std::format("{} < {}\n", a, b);
}
auto nan_cmp = nan <=> 1.0;
if (nan_cmp == std::partial_ordering::unordered) {
std::cout << "NaN is unordered with everything (including itself)\n";
}
// Reason: NaN != NaN is true per IEEE 754; this surprises many beginners
std::cout << std::format("NaN == NaN? {}\n", nan == nan);
}5. Using <=> with containers and algorithms
Classes with defaulted <=> work immediately in std::set, std::map, std::sort, and binary search.
Key Takeaways
- •Default <=> whenever member-wise comparison is correct — one line, six operators.
- •A custom <=> requires a separate == definition (default or manual).
- •Choose the return type carefully: strong_, weak_, or partial_ordering.
- •If any member is a double, the defaulted <=> returns partial_ordering.
- •Types with <=> work automatically in std::set, std::sort, etc.
int main() {
// ---- 1. Defaulted <=> ----
std::cout << "--- Defaulted <=> ---\n";
Point p1{1, 2}, p2{1, 3}, p3{1, 2};
std::cout << std::format("({},{}) == ({},{}): {}\n",
p1.x, p1.y, p3.x, p3.y, p1 == p3);
std::cout << std::format("({},{}) < ({},{}): {}\n",
p1.x, p1.y, p2.x, p2.y, p1 < p2);
std::cout << std::format("({},{}) > ({},{}): {}\n",
p2.x, p2.y, p1.x, p1.y, p2 > p1);
// ---- 5. Works in containers automatically ----
std::cout << "\n--- In Containers ---\n";
std::set<Point> points = {{3, 4}, {1, 2}, {1, 1}, {2, 0}};
std::cout << "Points in sorted order: ";
for (const auto& p : points) {
std::cout << std::format("({},{}) ", p.x, p.y);
}
std::cout << '\n';
// ---- 3. Custom <=> ----
std::cout << "\n--- Custom <=> (Version) ---\n";
Version v1{2, 0, 0}, v2{1, 9, 9}, v3{2, 0, 1};
std::cout << std::format("{} > {}: {}\n",
v1.to_string(), v2.to_string(), v1 > v2);
std::cout << std::format("{} < {}: {}\n",
v1.to_string(), v3.to_string(), v1 < v3);
std::cout << std::format("{} == {}: {}\n",
v1.to_string(), v1.to_string(), v1 == v1);
// Direct use of <=> result
auto cmp = v1 <=> v2;
if (cmp > 0) std::cout << std::format("{} is newer\n", v1.to_string());
else if (cmp < 0) std::cout << std::format("{} is newer\n", v2.to_string());
else std::cout << "Same version\n";
// Sort a vector of Versions
std::vector<Version> versions = {{1,0,0}, {3,2,1}, {2,0,0}, {1,5,3}};
std::ranges::sort(versions);
std::cout << "\nVersions sorted: ";
for (const auto& v : versions) {
std::cout << v.to_string() << " ";
}
std::cout << '\n';
// ---- 4. Partial ordering ----
partial_ordering_demo();
return 0;
}