With <=>
we have to make a decision: what do we want to do if the underlying comparison we need does not yet implement <=>
?
One option is: don't care. Just use <=>
and if the relevant types don't provide it, we're not comparable. That makes for a very concise implementation (note that you need to do the same thing for ==
for a total of six functions):
template <typename T>
class optional {
public:
// ...
template <typename U>
constexpr std::compare_three_way_result_t<T, U>
operator<=>(optional<U> const& rhs) const
{
if (has_value() && rhs) {
return **this <=> *rhs;
} else {
return has_value() <=> rhs.has_value();
}
}
template <typename U>
constexpr std::compare_three_way_result_t<T, U>
operator<=>(U const& rhs) const
{
if (has_value()) {
return **this <=> *rhs;
} else {
return strong_ordering::less;
}
}
constexpr strong_ordering operator<=>(nullopt_t ) const {
return has_value() ? strong_ordering::greater
: strong_ordering::equal;
}
};
The three-way comparison between bool
s yields std::strong_ordering
, which is implicitly convertible to the other comparison categories. Likewise, strong_ordering::less
being implicitly convertible to weak_ordering::less
, partial_ordering::less
, strong_equality::unequal
, or weak_equality::nonequivalent
, as appropriate.
The above is the super nice, happy answer. And hopefully as time progresses, people will adopt <=>
and more and more code will be able to rely on the happy answer.
Another option is: we do care, and want to fallback to synthesizing an ordering. That is, if we need to compare a T
and a U
and they don't provide a <=>
, we can use one of the new customization point objects in the standard library. Which one though? The most conservative option is to synthesize a partial_ordering
with compare_partial_order_fallback
. This guarantees that we will always get the right answer.
For the optional<T>
vs optional<U>
comparison, that looks like:
template <typename T>
class optional {
public:
// ...
template <typename U>
constexpr auto operator<=>(optional<U> const& rhs) const
-> decltype(std::compare_partial_order_fallback(**this, *rhs))
{
if (has_value() && rhs) {
return std::compare_partial_order_fallback(**this, *rhs);
} else {
return has_value() <=> rhs.has_value();
}
}
// ...
};
Unfortunately, as implemented above, our comparison now always returns partial_ordering
-- even between two optional<int>
s. So a better alternative might be to use whatever <=>
returns and use the conservative fallback otherwise. I have no idea how to name this concept yet, so I'll just go with:
template <typename T, std::three_way_comparable_with<T> U>
constexpr auto spaceship_or_fallback(T const& t, U const& u) {
return t <=> u;
}
template <typename T, typename U>
constexpr auto spaceship_or_fallback(T const& t, U const& u)
-> decltype(std::compare_partial_order_fallback(t, u))
{
return std::compare_partial_order_fallback(t, u);
}
and use that:
template <typename T>
class optional {
public:
// ...
template <typename U>
constexpr auto operator<=>(optional<U> const& rhs) const
-> decltype(spaceship_or_fallback(**this, *rhs))
{
if (has_value() && rhs) {
return spaceship_or_fallback(**this, *rhs);
} else {
return has_value() <=> rhs.has_value();
}
}
// ...
};
A third option, the most conservative option, is what the Standard Library will take. Provide both the relational operators and the three-way comparison:
template <typename T> class optional { /* ... */ };
template <typename T, typename U>
constexpr bool operator<(optional<T> const&, optional<U> const&);
template <typename T, typename U>
constexpr bool operator>(optional<T> const&, optional<U> const&);
template <typename T, typename U>
constexpr bool operator<=(optional<T> const&, optional<U> const&);
template <typename T, typename U>
constexpr bool operator>=(optional<T> const&, optional<U> const&);
template <typename T, std::three_way_comparable_with<T> U>
std::compare_three_way_result_t<T, U>
operator<=>(optional<T> const& x, optional<U> const& y) {
if (x && y) {
return *x <=> *y;
} else {
return x.has_value() <=> y.has_value();
}
}
This is the most conservative option as it effectively takes both the C++17-and-earlier comparison implementation strategy and the C++20 comparison implementation strategy. As in: the "Why not both?" strategy. The use of the concept
on operator<=>
is what ensures that a < b
invokes <=>
where possible, instead of <
.
It's by far the most tedious and verbose approach, with lots of boilerplate, but it ensures that for existing, single-object comparison types, existing comparisons continue to work and do the same thing. The standard library has to be conservative like this.
But new code doesn't.
a>>operator<=>>c;
that currently can be valid. – Atop