解决CRTP功能过载的歧义

时间:2019-07-10 12:15:52

标签: c++ templates c++17 crtp ambiguous

我想为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, 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;
}

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

3 个答案:

答案 0 :(得分:6)

首先,您需要一个特征来查看某物是否类似于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重载”。好东西。

答案 1 :(得分:2)

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

function isGreaterthanTen(val) {
  if(val > 10) {
    return true;
  } else {
    return false;
  }
}

基本上,当在第一个和第二个参数中传递CRTP时,将禁用第二个和第三个功能,仅保留第一个功能可用。

编辑:回答OP注释,如果#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; } T都继承第一个,将被调用,这不是预期的行为吗?

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

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

答案 2 :(得分:0)

在这种情况下,可以很方便地创建一个可以部分地专门用于执行此操作的帮助程序类,并将该函数转换为用于选择适当专门化的包装器:

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