C++ Logo

std-proposals

Advanced search

Re: Proposal for changing std::compare_three_way_result

From: Barry Revzin <barry.revzin_at_[hidden]>
Date: Fri, 5 Feb 2021 08:33:55 -0600
On Thu, Feb 4, 2021 at 1:27 PM Nicholas Schwab via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Dear all,
>
> currently, there is a slight discrepancy between the definitions of
> std::three_way_comparable_with and std::compare_three_way_result. To be
> precise, two types T and U can have a std::compare_three_way_result_t
> but not fulfill the concept std::three_way_comparable_with<T,U>. In my
> opinion, this is unfortunate and should be changed.
>
>
> == Current Status ==
>
> Consider the following structs
>
> struct A {
> auto operator<=>(const A&) const { return
> std::strong_ordering::equal; }
> };
>
> struct B {
> std::strong_ordering operator<=>(const B&) const = default; // this
> also declares operator==
> };
>
> All instances of either class will compare equal, in the sense that (x
> <=> y) == 0 for all x and y of either type A or type B. But, while B{}
> == B{} is well-formed, A{} == A{} is ill-formed. This stems from paper
> P1185 [1] which has good reasoning for this. Further, the following
> facts hold:
>
> static_assert(std::is_same_v<std::strong_ordering,
> std::compare_three_way_result_t<A,A>>);
> static_assert(std::is_same_v<std::strong_ordering,
> std::compare_three_way_result_t<B,B>>);
> static_assert(!std::three_way_comparable_with<A,A>);
> static_assert(std::three_way_comparable_with<B,B>);
>
> Code on Godbolt: https://godbolt.org/z/s97qeY
>
> This comes from the fact that std::compare_three_way_result_t<A,A> is
> basically declval(A <=> A) while std::three_way_comparable_with<A,A>
> also wants that A == A is well-formed (to be precise: the
> exposition-only concept __WeaklyEqualityComparableWith<A,A> holds) [2,3].
>
>
> == Problems arising from this ==
>
> I think that this is somewhat problematic, for multiple reasons. First,
> it is unintuitive (at least for me), types that have a three-way
> comparison result should also be three-way comparable (and vice versa).
>
> Second, std::optional<T>::operator<=>(const std::optional<U>&) requires
> std::three_way_comparable_with<T,U>. This means that while A{} <=> A{}
> is well-formed, std::optional<A>{} <=> std::optional<A>{} is not (both
> of these are well-formed for B)[4]. Looking at other STL-types, this is
> also the case for std::unique_ptr [5] and std::variant [6] but not for
> std::shared_ptr [7], std::pair [8] and std::tuple [9]. This behavior is
> unexpected. (Note that there is currently a bug in the gcc
> implementation of std::optional, hence this cannot be really checked
> [10]. I used std::variant on the godbolt example)
>
>
> == Possible Fixes ==
>
> I can think of four possible ways to deal with this:
>
> 1) Strengthen std::compare_three_way_result<T,U> such that it requires
> std::three_way_comparable_with<T,U> in order to have a "type" member
> alias. This would definitely address my first concern. For the second
> concern, this would ensure that std::compare_three_way_result<T,U> and
> std::compare_three_way_result<std::optional<T>, std::optional<U>> are
> equivalent. But, it would change nothing with regards to A{} <=> A{}
> being well-formed while std::optional<A>{} <=> std::optional<A>{} is not.
>
> 2) Weakening std::three_way_comparable_with<T,U> such that it only
> requires std::compare_three_way_result_t<T,U> exists. This would address
> both my first and second concern completely, making std::optional<A>{}
> <=> std::optional<A>{} well-formed.
>
> 3) Changing std::optional, std::unique_ptr, std::variant to not
> require std::three_way_comparable to have an operator<=>. This would
> address my concerns as 2) does. But it has the disadvantage that any
> user-defined type that requires three_way_comparable to declare its
> operator<=> has again the same problem. Since this is a very reasonable
> concept to require, I fear this would be error-prone.
>
> 4) Change the language such that it a program where there can be
> objects t, u such that t <=> u is well-formed while t == u is ill-formed
> (both in unevaluated context, so only declaring operator== without
> defining it would be fine), is illegal. This would be a pretty drastic
> change. It would address both my concerns by disallowing the type A.
> Further, one could argue that in many cases declaring operator<=>
> without declaring operator== is a mistake/unintentional. On the other
> hand there are certainly reasonable use-cases for having only relational
> compares but no equality comparison. This would push users to again
> define all four of <, <=, >= and >, which goes against the intention of
> operator<=>. The workaround of only declaring operator== would change a
> compilation error when operator== is used to be a linking error, which
> is inferior.
>
> Personally, I prefer fix 2) since it is both relatively easy and
> addresses all of my concerns. I would be very grateful for opinions and
> feedback on this.
>
> Best Wishes,
> Nicholas Schwab
>
>
> Sources:
>
> [1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1185r2.html
> [2] http://eel.is/c++draft/cmp#concept:three_way_comparable_with
> [3] http://eel.is/c++draft/cmp#result
> [4] http://eel.is/c++draft/optional.relops#lib:operator%3C=%3E,optional
> [5]
> http://eel.is/c++draft/unique.ptr.special#lib:operator%3C=%3E,unique_ptr
> [6] http://eel.is/c++draft/variant.relops#lib:operator%3C=%3E,variant
> [7]
>
> http://eel.is/c++draft/util.smartptr.shared.cmp#lib:operator%3C=%3E,shared_ptr
> [8] http://eel.is/c++draft/pairs.spec#lib:operator%3C=%3E,pair
> [9] http://eel.is/c++draft/tuple.rel#lib:operator%3C=%3E,tuple
> [10] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=98842
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals


5) Add equality to A.

A lot of the concepts in the standard library require more than the bare
minimum functionality. Whereas in C++17, we had LessThanComparable, which
only required that a < b is a valid strict, weak ordering, now we have
totally_ordered, which requires all six comparison operators.

In this case, it doesn't make sense to me to get to a situation with these
concepts such that three_way_comparable<A> holds but equality_comparable<A>
does not. It's clearly possible to implement equality in this case, at the
very least (a <=> b) == 0. This doesn't seem like a very burdensome
requirement, especially with the C++20 comparison rules which mean you
don't even have to implement operator!=.

There are some type traits in Ranges that are constrained, which would
suggest this implementation of compare_three_way_result_t:

template <typename T> using cref = std::remove_reference_t<T> const&;

template <typename T, three_way_comparable_with<T> U = T>
using compare_three_way_result_t = decltype(std::declval<cref<T>>() <=>
std::declval<cref<U>>());

But I'm not sure this is really a worthwhile change to make, mostly because
if types are three-way-comparable they should be equality-comparable.

Barry

Received on 2021-02-05 08:34:11