If you’ve ever written a C++ class that needs to be sortable or comparable, you know the pain: six operators to write, all of which have to agree with each other, all of which are easy to get subtly wrong. C++20 introduced a feature that collapses all of that boilerplate into a single line. It’s officially called the three-way comparison operator, but everyone calls it the spaceship operator because of how it looks: <=>.
In this post, we’ll walk through what it does, how you used to write comparisons before it existed, and how dramatically simpler things get once you adopt it.
What Is the Spaceship Operator?
The spaceship operator performs all three comparisons at once: less than, equal to, and greater than. Instead of returning a bool, it returns a value that tells you the relationship between two objects — whether the left side is less, equal, or greater.
You then either use that result directly, or — much more commonly — let the compiler use it to synthesize the traditional comparison operators (<, <=, >, >=, ==, !=) for you.
It returns one of three types from the <compare> header:
std::strong_ordering— values that compare equal are truly interchangeable (e.g., integers)std::weak_ordering— equal values compare equal but aren’t fully substitutable (e.g., case-insensitive strings)std::partial_ordering— some values may be incomparable (e.g., floating-pointNaN)
To see why this matters, let’s look at what we had to do before.
The Old Way: Six Operators by Hand
Imagine a simple Version struct with major, minor, and patch numbers. To make it fully comparable in pre-C++20 code, you had to write every operator yourself:
#include <iostream>
struct Version {
int major;
int minor;
int patch;
bool operator==(const Version& other) const {
return major == other.major
&& minor == other.minor
&& patch == other.patch;
}
bool operator!=(const Version& other) const {
return !(*this == other);
}
bool operator<(const Version& other) const {
if (major != other.major) return major < other.major;
if (minor != other.minor) return minor < other.minor;
return patch < other.patch;
}
bool operator>(const Version& other) const {
return other < *this;
}
bool operator<=(const Version& other) const {
return !(other < *this);
}
bool operator>=(const Version& other) const {
return !(*this < other);
}
};
int main() {
Version v1{1, 2, 3};
Version v2{1, 2, 5};
std::cout << std::boolalpha;
std::cout << "v1 < v2: " << (v1 < v2) << '\n'; // true
std::cout << "v1 == v2: " << (v1 == v2) << '\n'; // false
std::cout << "v1 >= v2: " << (v1 >= v2) << '\n'; // false
}
Six operators, about 25 lines of logic, and plenty of opportunities for a typo to create an inconsistency — for example, < and >= disagreeing on some edge case. Every time you add a new field to the struct, you have to remember to update three of these operators. It’s tedious and error-prone.
The New Way: A Custom Spaceship Operator
With C++20, you can replace all six operators with a single <=>. Here’s the non-defaulted version, where we write the comparison logic ourselves:
#include <compare>
#include <iostream>
struct Version {
int major;
int minor;
int 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;
}
bool operator==(const Version& other) const {
return major == other.major
&& minor == other.minor
&& patch == other.patch;
}
};
int main() {
Version v1{1, 2, 3};
Version v2{1, 2, 5};
std::cout << std::boolalpha;
std::cout << "v1 < v2: " << (v1 < v2) << '\n'; // true
std::cout << "v1 == v2: " << (v1 == v2) << '\n'; // false
std::cout << "v1 >= v2: " << (v1 >= v2) << '\n'; // false
}
Even though we wrote the comparison logic ourselves, we only had to write it once. The compiler uses our <=> to synthesize <, <=, >, and >= automatically. We only needed to define == separately, because when you write a custom (non-defaulted) <=>, the compiler doesn’t auto-generate == from it — for performance reasons, equality is treated separately.
This is already a huge win. But we can do better.
The Better Way: Just Default It
Most of the time, comparing structs member-by-member in declaration order is exactly what you want. When that’s the case, you can let the compiler write the whole thing for you with = default:
#include <compare>
#include <iostream>
struct Version {
int major;
int minor;
int patch;
auto operator<=>(const Version&) const = default;
};
int main() {
Version v1{1, 2, 3};
Version v2{1, 2, 5};
std::cout << std::boolalpha;
std::cout << "v1 < v2: " << (v1 < v2) << '\n'; // true
std::cout << "v1 == v2: " << (v1 == v2) << '\n'; // false
std::cout << "v1 >= v2: " << (v1 >= v2) << '\n'; // false
}
One line. That’s it. Every comparison operator works correctly.
How Does Defaulting Actually Work?
When you write = default on <=>, you’re telling the compiler: “Generate the comparison by comparing each member, in the order I declared them.” The compiler effectively writes something like this for you:
auto 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;
}
This is lexicographic comparison — the same way a dictionary sorts words. To compare “apple” vs “apricot”, you go letter by letter: a == a, then p == p, then p < r — done. The struct version does the same thing with members instead of letters.
There’s a critical implication here: declaration order matters. The compiler uses the order you wrote the members in, not alphabetical order or anything else. If you reordered the Version struct to put patch first, then Version{9, 9, 1} would suddenly be considered greater than Version{0, 0, 2} — almost certainly not what you want. So when using defaulted comparison, always declare your members in the order you want them prioritized.
One more nice thing: defaulting works recursively. As long as every member is itself comparable, the defaulted version “just works”:
struct ReleaseInfo {
Version version; // has its own <=>
std::string codename; // std::string has <=>
int build_number; // built-in
auto operator<=>(const ReleaseInfo&) const = default;
};
This compares version first (which itself compares major, minor, patch), then codename alphabetically, then build_number. All from a single line. And unlike with a custom <=>, a defaulted <=> does give you == for free too.
Conclusion: Why “Spaceship”?
The spaceship operator is one of those rare language features that’s both a massive ergonomic improvement and a genuine bug-prevention tool. You go from six hand-written operators that have to stay consistent with each other, to a single line the compiler can’t get wrong.
As for the name? Just look at it: <=>. Tilt your head a little. With the angle brackets as wings and the equals sign as the body, it looks like a tiny flying saucer cruising across your screen. The name stuck because it’s far more memorable than “three-way comparison operator” — and honestly, after using it for a while, you start to appreciate how much boilerplate that little spaceship beams away.
If you’re working in C++20 or later, there’s no reason not to reach for <=> whenever you need a comparable type. Compile with -std=c++20 on GCC or Clang, or /std:c++20 on MSVC, and let the spaceship do the work.