C++ Logo

std-proposals

Advanced search

Proposal for changing std::compare_three_way_result

From: Nicholas Schwab <cpp_std_at_[hidden]>
Date: Thu, 4 Feb 2021 20:26:41 +0100
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

Received on 2021-02-04 13:26:54