Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing operator<=> for optional<T>

With operator<=> being added into C++20, I wanted to try to reason about how to implement this operator for those cases where it's not a simple member-wise comparison.

How would you implement the spaceship operator for comparing an optional<T> to an optional<U>or a U, which is a case where we either have to compare the T to the U or compare underlying states, getting the correct return type? There isn't such an example in the latest paper.

like image 379
Barry Avatar asked Nov 15 '17 19:11

Barry


1 Answers

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 bools 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.

like image 183
Barry Avatar answered Oct 13 '22 21:10

Barry