Date: Thu, 10 Jun 2021 22:57:18 -0700
Hello,
*Summary:* std::equality_comparable_with should be extended to support more
types that have a heterogeneous equality, specifically types which are
move-only or are otherwise burned by one particular aspect of the common
reference requirements.
*Background: *std::equality_comparable_with<T, U> checks that const T& and
const U& are both convertible to their std::equality_comparable common
reference. This requirement is because we want
std::equality_comparable_with to specify equality rather than any number of
things that are not equality (e.g. iterator/sentinel "equality", DSLs).
However, this requirement is too strict, and I believe we can do better.
https://stackoverflow.com/q/66937947/1896169 is relevant and why I've been
thinking about this; it shows that
std::equality_comparable_with<std::unique_ptr<T>, std::nullptr_t> is false,
even though the heterogenous operator== is in fact an actual equality.
Why we have the common-reference requirement: cross-type equality isn't
mathematically rigorously defined (see https://wg21.link/n3351 pages
15-16). When we write operator==(T, U) as an equality operator, what we are
actually saying is that there is some common super-type of T and U under
which this operator== is an equality. This is what
std::equality_comparable_with attempts to encode. We don't actually need to
convert to this common supertype to do t == u; we just need the
heterogeneous operator== to be equivalent to the supertype's operator==.
*Proposal: *I believe the common *reference *requirement is too strict.
Mathematically, a "reference" is meaningless. All we actually need is a
common "supertype." This is important, because sometimes
std::common_reference<const T&, const U&> is a non-reference V, such as
with the unique_ptr<T>-nullptr_t case, where it is unique_ptr<T>. By
specifying equality_comparable_with to require a common *reference*, we end
up requiring that unique_ptr<T> is copyable (const unique_ptr<T>& must be
convertible to unique_ptr<T>). In this case and many other similar cases
with a heterogeneous operator== that means equality, we can capture this
equality with equality_comparable_with if change the convertible_to<const
T&, common_reference_t<const T&, const U&>> to allow const T& to be the
same type as common_reference_t<const T&, const U&> after stripping cvref.
This does mean that it will become very difficult to express an equals()
function in terms of the supertype operator==, but the point of the
supertype operator== requirement is that it exists, not that it will be
used.
In the code form that I believe captures this (quoting my answer to that
stackoverflow question):
> template <class T, class U>concept equality_comparable_with =
> __WeaklyEqualityComparableWith<T, U> &&
> std::equality_comparable<T> &&
> std::equality_comparable<U> &&
> std::equality_comparable<
> std::common_reference_t<
> const std::remove_reference_t<T>&,
> const std::remove_reference_t<U>&>> &&
> __CommonSupertypeWith< // not necessarily a reference anymore
> const std::remove_reference_t<T>&,
> const std::remove_reference_t<U>&>;
> template <class T, class U>concept __CommonSupertypeWith =
> std::same_as<std::common_reference_t<T, U>, std::common_reference_t<U, T>> &&
> std::convertible_to<T, const std::common_reference_t<T, U>&> &&
> std::convertible_to<U, const std::common_reference_t<T, U>&>;
>
> *"Diff":*
Changes this: std::common_reference_with<const remove_reference_t<T>&,
const remove_reference_t<U>&>
To this: __CommonSupertypeWith<const remove_reference_t<T>&, const
remove_reference_t<U>&>
Which __CommonSupertypeWith is the same as common_reference_with, except we
specify the convertibility to the common_reference_t as convertibility to const
(common-reference)& instead of (common-reference).
*Other thoughts:*
I have other thoughts regarding equality_comparable_with, namely that I
think it should be possible to opt-in to equality_comparable_with without
hijacking common_reference_t for your type, which is problematic because
common_reference_t is not exclusive to equality_comparable_with. That is, I
think there should be an explicit customization point to declare "MyType's
heterogeneous operator== is actually equality and isn't just using the '=='
syntax." However, I don't feel as strongly about this.
Thanks,
Justin Bassett
*Summary:* std::equality_comparable_with should be extended to support more
types that have a heterogeneous equality, specifically types which are
move-only or are otherwise burned by one particular aspect of the common
reference requirements.
*Background: *std::equality_comparable_with<T, U> checks that const T& and
const U& are both convertible to their std::equality_comparable common
reference. This requirement is because we want
std::equality_comparable_with to specify equality rather than any number of
things that are not equality (e.g. iterator/sentinel "equality", DSLs).
However, this requirement is too strict, and I believe we can do better.
https://stackoverflow.com/q/66937947/1896169 is relevant and why I've been
thinking about this; it shows that
std::equality_comparable_with<std::unique_ptr<T>, std::nullptr_t> is false,
even though the heterogenous operator== is in fact an actual equality.
Why we have the common-reference requirement: cross-type equality isn't
mathematically rigorously defined (see https://wg21.link/n3351 pages
15-16). When we write operator==(T, U) as an equality operator, what we are
actually saying is that there is some common super-type of T and U under
which this operator== is an equality. This is what
std::equality_comparable_with attempts to encode. We don't actually need to
convert to this common supertype to do t == u; we just need the
heterogeneous operator== to be equivalent to the supertype's operator==.
*Proposal: *I believe the common *reference *requirement is too strict.
Mathematically, a "reference" is meaningless. All we actually need is a
common "supertype." This is important, because sometimes
std::common_reference<const T&, const U&> is a non-reference V, such as
with the unique_ptr<T>-nullptr_t case, where it is unique_ptr<T>. By
specifying equality_comparable_with to require a common *reference*, we end
up requiring that unique_ptr<T> is copyable (const unique_ptr<T>& must be
convertible to unique_ptr<T>). In this case and many other similar cases
with a heterogeneous operator== that means equality, we can capture this
equality with equality_comparable_with if change the convertible_to<const
T&, common_reference_t<const T&, const U&>> to allow const T& to be the
same type as common_reference_t<const T&, const U&> after stripping cvref.
This does mean that it will become very difficult to express an equals()
function in terms of the supertype operator==, but the point of the
supertype operator== requirement is that it exists, not that it will be
used.
In the code form that I believe captures this (quoting my answer to that
stackoverflow question):
> template <class T, class U>concept equality_comparable_with =
> __WeaklyEqualityComparableWith<T, U> &&
> std::equality_comparable<T> &&
> std::equality_comparable<U> &&
> std::equality_comparable<
> std::common_reference_t<
> const std::remove_reference_t<T>&,
> const std::remove_reference_t<U>&>> &&
> __CommonSupertypeWith< // not necessarily a reference anymore
> const std::remove_reference_t<T>&,
> const std::remove_reference_t<U>&>;
> template <class T, class U>concept __CommonSupertypeWith =
> std::same_as<std::common_reference_t<T, U>, std::common_reference_t<U, T>> &&
> std::convertible_to<T, const std::common_reference_t<T, U>&> &&
> std::convertible_to<U, const std::common_reference_t<T, U>&>;
>
> *"Diff":*
Changes this: std::common_reference_with<const remove_reference_t<T>&,
const remove_reference_t<U>&>
To this: __CommonSupertypeWith<const remove_reference_t<T>&, const
remove_reference_t<U>&>
Which __CommonSupertypeWith is the same as common_reference_with, except we
specify the convertibility to the common_reference_t as convertibility to const
(common-reference)& instead of (common-reference).
*Other thoughts:*
I have other thoughts regarding equality_comparable_with, namely that I
think it should be possible to opt-in to equality_comparable_with without
hijacking common_reference_t for your type, which is problematic because
common_reference_t is not exclusive to equality_comparable_with. That is, I
think there should be an explicit customization point to declare "MyType's
heterogeneous operator== is actually equality and isn't just using the '=='
syntax." However, I don't feel as strongly about this.
Thanks,
Justin Bassett
Received on 2021-06-11 00:57:37