We’ll start with the optional on optional comparison. There are four cases to consider: both on, left on only, right on only, and both off. That leads us to our first approach:

The spaceship operator returns one of five different comparison categories: strong_ordering , weak_ordering , partial_ordering , strong_equality , or weak_equality . Each of these categories has defined named numeric values. In the paper, the categories are presented in a way that indicates the direction in which they implicitly convert in a really nice way, so I’m just going to copy that image here (all credit to Herb Sutter):

The comparison categories for the spaceship operator

Likewise, their table of values:

Just carefully perusing this table, it’s obvious that our first implementation is totally wrong. strong_ordering has numeric values for less , equal , and greater … but the rest don’t! In fact, there is no single name that is common to all 5. By implementing it the way we did, we’ve reduced ourselves to only supporting strong orderings.

So if we can’t actually name the numeric values, what do we do? How can we possibly do the right thing?

Here, we can take advantage of a really important aspect of the comparison categories: convertibility. Each type is convertible to all of its less strict versions, and each value is convertible to its less strict equivalents. strong_ordering::greater can become weak_ordering::greater or partial_ordering::greater or strong_equality::nonequal or weak_equality::nonequivalent . And the way we can take advantage of this is to realize that we don’t really have four cases, we have two: both on, and not that. Once we’re in the “not” case, we don’t care about the values anymore, we only care about the bools. And we already have a way to do a proper 3-way comparison: <=> !

The shapeship operator for bool s gives us a strong_ordering , which is convertible to everything. So that part is guaranteed to work and do the right thing (I encourage you to work through the cases and verify that this is indeed the case).

But this still isn’t quite right. The problem is actually <=> (thanks, Captain Obvious?). You see, while a < b is allowed to fallback to a <=> b < 0 , the reverse is not true. a <=> b is not allowed to call anything else (besides b <=> a ). It either works, or it fails. By using the spaceship operator directly on our values, we’re actually reducing ourselves to only those modern types that support 3-way comparison. Which, so far, is no user-defined types. Moreover, <=> doesn’t support mixed-integer comparisons, so even for those types that come with builtin spaceship support (that’s a fantastic phrase), we would effectively disallow comparing an optional<int> to an optional<long> . So, this operator in this particular context isn’t very useful.

So what are we to do? Re-implement 3-way comparison ourselves manually? Nope, that’s what the library is for! Along with language support for the spaceship operator, C++20 will also come with several handy library functions. The relevant one for us is std::compare_3way() . This one will do the fallback: it prefers <=> , but if not will try the normal operators and is smart enough to know whether to return strong_ordering or strong_equality . And it’s SFINAE-friendly. Which means for our purposes, we can just drop-in replace our too-constrained version with it:

And I think we’re done.