解决 CRTP 函数重载歧义问题



我有几个函数,我想为 CRTP 基类的派生类工作。问题是,如果我将派生类传递到 CRTP 类的自由函数中,就会产生歧义。说明这一点的一个最小示例是以下代码:

template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b) 
{
    std::cout << "LT, RTn";
}
template<typename T, typename U>
void fn(const T a, const A<U>& b)
{
    std::cout << "L, RTn";
}
template<typename T, typename U>
void fn(const A<T>& a, const U& b)
{
    std::cout << "LT, Rn";
}
int main()
{
    C a; // if we change C to A<C> everything works fine
    B b;
    fn(a,a); // fails to compile due to ambiguous call
    fn(b,a);
    fn(a,b);
    return 0;
}

理想情况下,我希望它适用于派生类,就像我要使用基类一样(不必为基类重新定义所有内容,CRTP 习惯用法的全部意义在于不必为多个类定义 fn(。

首先,你需要一个特征来查看某物是否A相似。您不能在这里只使用is_base_of,因为您不知道将从哪个A继承。我们需要使用额外的间接寻址:

template <typename T>
auto is_A_impl(A<T> const&) -> std::true_type;
auto is_A_impl(...) -> std::false_type;
template <typename T>
using is_A = decltype(is_A_impl(std::declval<T>()));

现在,我们可以利用这个特征来写出我们的三个重载:两个重载A,只有左A,只有右重A

#define REQUIRES(...) std::enable_if_t<(__VA_ARGS__), int> = 0
// both A
template <typename T, typename U, REQUIRES(is_A<T>() && is_A<U>())
void fn(T const&, U const&);
// left A
template <typename T, typename U, REQUIRES(is_A<T>() && !is_A<U>())
void fn(T const&, U const&);
// right A
template <typename T, typename U, REQUIRES(!is_A<T>() && is_A<U>())
void fn(T const&, U const&);

请注意,我只是在这里TU,我们不一定想失望和丢失信息。

<小时 />

关于 C++20 中出现的概念的一个好处是编写这个是多么容易。这两个特征,现在成为一个概念:

template <typename T> void is_A_impl(A<T> const&);
template <typename T>
concept ALike = requires(T const& t) { is_A_impl(t); }

还有三个重载:

// both A
template <ALike T, ALike U>
void fn(T const&, U const&);
// left A
template <ALike T, typename U>
void fn(T const&, U const&);
// right A
template <typename T, ALike U>
void fn(T const&, U const&);

语言规则已经强制要求在可行时首选"两个 A"重载。好东西。

鉴于在您的示例中,第二个函数的第一个元素和第三个函数的第二个元素不应从 CRTP 继承,您可以尝试如下操作:

#include<iostream>
#include<type_traits>
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b) 
{
    std::cout << "LT, RTn";
}
template<typename U>
struct isNotCrtp{
    static constexpr bool value = !std::is_base_of<A<U>, U>::value; 
};
template<typename T, typename U, std::enable_if_t<isNotCrtp<T>::value, int> = 0>
void fn(const T a, const A<U>& b)
{
    std::cout << "L, RTn";
}
template<typename T, typename U, std::enable_if_t<isNotCrtp<U>::value, int> = 0>
void fn(const A<T>& a, const U& b)
{
    std::cout << "LT, Rn";
}
int main()
{
    C a; 
    B b;
    fn(a,a); 
    fn(b,a);
    fn(a,b);
    return 0;
}
基本上,我们在第一个和第二个

参数中传递 CRTP 时禁用第二个和第三个函数,只留下第一个函数可用。

编辑:回答OP评论,如果TU都继承了第一个将被调用,这不是预期的行为吗?

在以下位置玩代码: https://godbolt.org/z/ZA8hZz

编辑:有关更一般的答案,请参阅用户Barry发布的答案

这是创建

可以部分专用于执行此操作的帮助程序类很方便的情况之一,该函数转换为选择适当专用化的包装器:

#include <iostream>
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
struct fn_helper {
    static void fn(const T &a, const U &b)
    {
        std::cout << "L, Rn";
    }
};
template<typename T, typename U>
struct fn_helper<T, A<U>> {
    static void fn(const T &a, const A<U> &b)
    {
        std::cout << "L, RTn";
    }
};
template<typename T, typename U>
struct fn_helper<A<T>, U> {
    static void fn(const A<T> &a, const U &b)
    {
        std::cout << "LT, Rn";
    }
};
template<typename T, typename U>
struct fn_helper<A<T>, A<U>> {
    static void fn(const A<T> &a, const A<U> &b)
    {
        std::cout << "LT, RTn";
    }
};
template<typename T, typename U>
void fn(const T &a, const U &b)
{
    fn_helper<T,U>::fn(a, b);
}
int main()
{
    A<C> a;
    B b;
    fn(a,a);
    fn(b,a);
    fn(a,b);
    return 0;
}

输出(GCC 9(:

LT, RT
L, RT
LT, R

我希望现代C++编译器只需要选择它们最适度的优化级别来完全优化包装函数调用。

最新更新