解决 CRTP 函数重载歧义

Resolving CRTP function overload ambiguity

我有几个函数想用于 CRTP 基础 class 的派生 classes。问题是,如果我将派生的 classes 传递到用于 CRTP class 的自由函数中,就会出现歧义。一个最小的例子来说明这一点是这个代码:

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, RT\n";
}

template<typename T, typename U>
void fn(const T a, const A<U>& b)
{
    std::cout << "L, RT\n";
}

template<typename T, typename U>
void fn(const A<T>& a, const U& b)
{
    std::cout << "LT, R\n";
}

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

理想情况下,我希望这适用于派生的 classes,就像我使用基础 class 一样(无需重新定义基础 class 的所有内容) es,CRTP 习语的全部要点是不必为多个 classes).

定义 fn

在这种情况下,可以很方便地创建可以部分特化来执行此操作的助手 class,并将函数转换为选择适当特化的包装器:

#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, R\n";
    }
};

template<typename T, typename U>
struct fn_helper<T, A<U>> {
    static void fn(const T &a, const A<U> &b)
    {
        std::cout << "L, RT\n";
    }
};

template<typename T, typename U>
struct fn_helper<A<T>, U> {
    static void fn(const A<T> &a, const U &b)
    {
        std::cout << "LT, R\n";
    }
};

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, RT\n";
    }
};

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++ 编译器只需要选择最适度的优化级别来完全优化包装函数调用。

鉴于在您的示例中,第二个函数的第一个元素和第三个函数的第二个元素不应继承自 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, RT\n";
}

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, RT\n";
}

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, R\n";
}

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

发布的答案

首先,您需要一个特征来查看某物是否类似于 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&);

语言规则已经强制 "both A" 重载在可行时是首选。好东西。