4

Suppose I have a MyCustomType that has comparisons with SomeOtherType:

struct SomeOtherType {
    int value;

    constexpr bool operator==(const SomeOtherType& rhs) const = default;
};

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;
    }
};

This is great, but static_assert(std::equality_comparable_with<MyCustomType, SomeOtherType>); fails, meaning that I can't use them for heterogenous lookup in the std::ranges algorithms:

error: no type named 'type' in 'struct std::common_reference<const MyCustomType&, const SomeOtherType&>'

I understand why this fails: we may have an operator==, but we don't meet the common-reference requirements (see also Does `equality_­comparable_with` need to require `common_reference`? ). However, my type is, practically speaking, equality comparable with SomeOtherType. How can I convince the compiler that this is the case?

Justin
  • 24,288
  • 12
  • 92
  • 142
  • Why not implement `const bool operator==(const SomeOtherType& sot) const;`? – Thomas Matthews Apr 04 '21 at 18:29
  • @ThomasMatthews Because I accidentally mixed terminology when I wrote this. `SomeOtherType` is `int` in this case. I'll come through and clarify this. – Justin Apr 04 '21 at 18:30

1 Answers1

2

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.

Justin
  • 24,288
  • 12
  • 92
  • 142
  • This feels so icky to me. `std::common_reference_t` isn't just for equality AFAIK, so doing this seems questionable – Justin Apr 08 '21 at 05:27