Note that this post contains detailed explanations that are likely to end up incorrect as this feature evolves to be accepted as part of the standard. Specifically there is an ongoing matter of discussion whether defaulting
operator<=> should generate equality comparisons, and it likely will not do this in the end. Whenever this situation clears up more, this post will be corrected.
This post still provides value as a way to get a broad understanding of this feature, just keep in mind that the details are subject to change as C++20 is not yet finalised.
Three-way comparison is another great C++20 addition to the language. Personally I really like this feature as it lets us get rid of boilerplate code!
In particular we’re talking about the boilerplate code that arises when we want our custom types to be comparable using
== != < <= > >=.
As you can see, even for this very trivial type
T, we have to do quite a bit of work to implement the expected baseline of comparison operators. It’s less painful than it could have been, since the only real complex work is done in
operator< and the other operators are derived from that. But there is still quite some room for subtle bugs due to brain farts or copy-paste errors. Bugs in comparison operators tend to cause extra headache since they might not surface until later on when used in some complicated algorithm. Yuck.
So what about three-way comparison?
The three-way comparison operator is a brand new binary operator for comparing two values. It looks like
a <=> b (it is visually kinda similar to a spaceship, which is why many people call it the spaceship operator). It returns a value that can be compared to
0 to establish the relation.
Here we have established the full set of relations between
b using just one single operator call between them. That’s pretty cool. We can already see a potential benefit in performance if comparing the operands is expensive, as we now need to do it just once. This is the same idea as the C string compare function
At this point we might be tempted to assume that the return value of
int, but it is actually not. C++20 took the opportunity to make
operator<=> able to convey even more information to both the users and the compiler by defining a few possible return types: std::strong_ordering, std::weak_ordering, std::partial_ordering, std::strong_equality and std::weak_equality.
These all provide different guarantees on how values can be compared. I won’t go into details about all of them in this post, but
std::strong_ordering gives the strongest guarantees and implies that if
b are equivalent in ordering (i.e.
a < b == false && b < a == false) then they are also indistinguishable (i.e.
a == b and
f(a) == f(b) where
f is a pure function).
A value of the type
std::strong_ordering is returned when
<=> is used on integers, for example. Floats, on the other hand, do not fulfil the criteria of
std::strong_ordering since they might be equivalent but still distinguishable (
0.0f are equivalent in ordering, but are still distinguishable since for example
std::signbit would return different results). There are also floating point values that are not orderable at all, like
NaN. Because of this,
<=> returns the type
std::partial_ordering for floats.
All of these comparison category types define values. For example there is
std::strong_ordering::less which is returned by
3 <=> 6 and
std::partial_ordering::unordered which is returned by
0.5f <=> NAN. They also provide comparison operators that work against the literal
0 which is how you can use them like
a <=> b < 0. Note that comparing them against anything else than the literal
0 is undefined behaviour.
Killing that boilerplating
So far so good, but how does this eliminate the boilerplating from before? Well, since the
operator<=> can be used to describe all other comparison operators in one call as we did above, and it also provides semantic information on the ordering itself using the return type, the compiler is actually kind enough to generate all other operators if the
operator<=> is defined properly!
Let’s look at the example type from above, but with an extra inlined friend function.
As you can see, implementing it is pretty straightforward, at least for this case. The return type will be
std::strong_ordering as you might have guessed, since that is what the operator returns when comparing ints. If one of the members would have been say, a float, then our function would have to return
std::partial_ordering since it is the weakest comparison category amongst all the members. There is a helper template called std::common_comparison_category for working out the appropriate return type.
But really. For the majority of cases, we can take an even simpler approach:
This will make the compiler auto generate a default implementation of the three-way comparison operator which will lexiographically compare all members (from top to bottom) in the same manner as our manually implemented version, and it will also work out the correct return type based on the common comparison category of the members involved. Phew! Not bad.
Yay for auto generation
At this point where our type
T has an
operator<=> defined that returns one of the comparison category types, we will get the other comparison operators automatically generated for free!
The auto generated operators will be implemented to use the
operator<=> function internally for their logic. If the return type is
std::partial_ordering, then we get all of the operators defined (that is
== != < <= > >=) and if the return type is
std::weak_equality then we get
== != but not
< <= > >=.
In other words, in the past we needed to define six operator functions manually to get basic comparisons. With this new feature we can
= default a single operator and get all the comparison operators for free. This is a huge win since we can get rid of cluttery and potentially buggy code.
In my opinion, C++20 is great since it has many of these things that let you write simpler and more correct code while typing fewer lines. This is why C++20 is so exciting.
If you’re curious about other C++20 features, make sure to check out my other posts below.