Philosophically, the common-reference requirements of std::equality_comparable_with
are explicitly encoding the implicit statement made when writing a heterogenous operator==(T, U)
to actually mean equality*: there is some common supertype "T
union U
" for which the operator==
is equality. This "T
union U
" doesn't actually exist in the code for MyCustomType
and SomeOtherType
. If we make the type actually exist by specializing std::common_reference_t
, then we can meet std::equality_comparable_with
.
*Some types use operator==
for equivalence rather than equality (e.g. iterators+sentinels), and as such should not and do not meet std::equality_comparable_with
.
We can use the std::basic_common_reference
customization point to specify a proxy reference:
The class template basic_common_reference
is a customization point that allows users to influence the result of common_reference
for user-defined types (typically proxy references).
For this to work, we need:
eq_proxy_ref<T>
which acts like a reference for T
.
MyCustomType
must be implicitly convertible to eq_proxy_ref<T>
.
SomeOtherType
must be implicitly convertible to eq_proxy_ref<T>
.
basic_common_reference
of MyCustomType
and SomeOtherType
must return this eq_proxy_ref<int>
. A eq_proxy_ref<MyCustomProxy>
could work too if you wanted to avoid leaking the internals of MyCustomType
.
eq_proxy_ref<T>
must have comparison operators between itself.
eq_proxy_ref<T>
must obey the spirit of the requirement.
Beware, however, that std::common_reference_t
is used for more than just equality, including std::three_way_comparable_with
, std::totally_ordered_with
, and some of the Ranges algorithms or views. As such, your eq_proxy_ref<T>
should actually be a common reference for your two types, not just a mechanism to enable equality.
An example meeting these constraints follows:
#include <concepts>
#include <type_traits>
// Assuming you don't own SomeOtherType:
template <typename T>
class MyCustomTypeEqProxy {
template <typename>
friend class MyCustomTypeEqProxy;
private:
T ref_;
public:
template <typename U>
requires std::convertible_to<U, T>
constexpr MyCustomTypeEqProxy(U ref)
: ref_(ref)
{}
constexpr MyCustomTypeEqProxy(const SomeOtherType& rhs)
requires std::convertible_to<const int&, T>
: ref_(rhs.value)
{}
template <typename U>
requires std::equality_comparable_with<T, U>
constexpr bool operator==(const MyCustomTypeEqProxy<U>& rhs) const {
return ref_ == rhs.ref_;
};
};
struct MyCustomType {
int x;
constexpr bool operator==(const MyCustomType& rhs) const = default;
constexpr bool operator==(const SomeOtherType& rhs) const {
return x == rhs.value;
}
friend constexpr bool operator==(const SomeOtherType& lhs, const MyCustomType& rhs) {
return lhs.value == rhs.x;
}
constexpr operator MyCustomTypeEqProxy<int>() const { return MyCustomTypeEqProxy<int>(x); }
};
namespace std {
// May not be needed, but allows the custom proxy reference to expand to common references
// of what we're comparing against.
template <typename T, typename U, template <typename> class TQ, template <typename> class UQ>
struct basic_common_reference<::MyCustomTypeEqProxy<T>, U, TQ, UQ> {
using type = ::MyCustomTypeEqProxy< std::common_reference_t<T, UQ<U>> >;
};
template <typename T, typename U, template <typename> class TQ, template <typename> class UQ>
struct basic_common_reference<T, ::MyCustomTypeEqProxy<U>, TQ, UQ> {
using type = ::MyCustomTypeEqProxy< std::common_reference_t<TQ<T>, U> >;
};
// Tell std::common_reference_t about MyCustomTypeEqProxy
template <template <typename> class LQ, template <typename> class RQ>
struct basic_common_reference<::MyCustomType, ::SomeOtherType, LQ, RQ> {
using type = ::MyCustomTypeEqProxy<int>;
};
template <template <typename> class LQ, template <typename> class RQ>
struct basic_common_reference<::SomeOtherType, ::MyCustomType, LQ, RQ> {
using type = ::MyCustomTypeEqProxy<int>;
};
}
Compiler Explorer link
I suspect that I missed some nuances, but this is enough to meet std::equality_comparable_with
.