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).
Frequently Asked Questions
QWhen should I customize <=> instead of using = default?
QWhat is the difference between strong, weak, and partial ordering?
QIs <=> compatible with C++17 code that uses < and ==?
QWhy must I define operator== separately when I write a custom <=>?
#include <iostream>
#include <format>
#include <compare>
#include <string>
#include <set>
#include <vector>
#include <algorithm>
#include <cmath>1 Defaulted <=> — one line gives you all 6 operators
operator<=> defines three-way comparison and can synthesize relational operators.
Use this when types need ordering/equality semantics.
It centralizes comparison logic and reduces boilerplate.
Default or implement operator<=> and define operator== when needed.
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
Comparison categories describe whether ordering is strong, weak, or partial for a type.
Use this when you need to reason about strong, weak, or partial ordering semantics.
Choosing the correct category makes ordering guarantees explicit to generic algorithms.
Return `std::strong_ordering`, `std::weak_ordering`, or `std::partial_ordering` as appropriate.
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
operator<=> defines three-way comparison and can synthesize relational operators.
Use this when types need ordering/equality semantics.
It centralizes comparison logic and reduces boilerplate.
Default or implement operator<=> and define operator== when needed.
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
Floating-point comparisons often produce `std::partial_ordering` because NaN is unordered.
Use this when compared values can be unordered (for example, floating-point NaN cases).
NaN semantics mean some values are neither less, equal, nor greater, and code must handle that case.
Check `is_ordered()`/equivalent outcomes and avoid assuming total ordering for floating-point data.
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
operator<=> defines three-way comparison and can synthesize relational operators.
Use this when types need ordering/equality semantics.
It centralizes comparison logic and reduces boilerplate.
Default or implement operator<=> and define operator== when needed.
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;
}