Is there a way to use hidden friends when using (recursive) constraints?

Viewed 66

Suppose one has a class for which one wants to define hidden friends, e.g. heterogeneous comparison operators:

#include <concepts>

template <typename T> struct S;

template <typename C> constexpr bool is_S = false;
template <typename T> constexpr bool is_S<S<T>> = true;

template <typename T>
struct S {
    using type = T;
    T data;

    constexpr S() : data() {}
    constexpr explicit S(const T &t) : data(t) {}

    template <typename U>
    requires 
        (!is_S<U>) && std::equality_comparable_with<T, U>
    friend constexpr bool operator==(const S &s, const U &u) { return s.data == u; }
};

// pre-existing, not hidden friend
template <typename T>
constexpr bool operator==(const S<T> &a, const S<T> &b) { return a.data == b.data; }

The code above seems reasonable, but it doesn't work due to CWG2369's resolution -- which at the moment is only implemented by GCC (>=11). The resolution make the constraint resolution have endless recursion, for instance when using something like this:

template <typename T> 
struct wrapper
{
    T value;
};

template <typename T, typename U>
requires std::equality_comparable_with <T, U>
constexpr bool operator==(const wrapper<T>& a, const wrapper<U>& b)
{
    return a.value == b.value;
}

using SD = S<double>;
static_assert(std::equality_comparable<wrapper<SD>>); // ERROR, recursive constraints

The solution in this case should be to constrain S's operator== in a non-dependent way:

    template <typename V, typename U>
    requires 
        is_S<V> && (!is_S<U>) && std::equality_comparable_with<typename V::type, U>
    friend constexpr bool operator==(const V &v, const U &u) { return v.data == u; }

But now this does not depend on a specific S specialization any more, and therefore will cause redefinition errors:

S<int> s1;
S<double> s2; // ERROR: redefinition of operator==

(Godbolt for all of the above.)

Am I missing something or the above solution for the recursive constraints is fundamentally incompatible with hidden friends?

1 Answers

Here's a possible solution: defining the operator in a non-template empty base class:

class S_base 
{
    template <typename V, typename U>
    requires 
        is_S<V> && (!is_S<U>) && std::equality_comparable_with<typename V::type, U>
    friend constexpr bool operator==(const V &v, const U &u) { return v.data == u; }
};

Then have S privately inherit from it:

template <typename T>
struct S : private S_base 
{
    using type = T;
    T data;

    constexpr S() : data() {}
    constexpr explicit S(const T &t) : data(t) {}
};

Many thanks to Patrick Palka for suggesting this approach.

Related