使用(递归)约束时有没有办法使用隐藏的朋友?

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

假设有人 class 想为其定义隐藏的朋友,例如异构比较运算符:

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

上面的代码似乎很合理,但由于 CWG2369 的决议,它无法正常工作——目前仅由 GCC (>=11) 实现。该决议使约束决议有无限递归,例如当使用这样的东西时:

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

这种情况下的解决方案应该以非依赖的方式约束Soperator==

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

但现在这不再依赖于特定的 S 专业化,因此会导致重新定义错误:

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

Godbolt 以上所有。)

我是不是遗漏了什么,或者上面的递归约束解决方案与隐藏的朋友根本不兼容?

这是一个可能的解决方案:在非模板空基中定义运算符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; }
};

然后让S私下继承它:

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

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

非常感谢 Patrick Palka 建议采用这种方法。