Устранение неоднозначности перегрузки функции CRTP - PullRequest
5 голосов
/ 10 июля 2019

У меня есть несколько функций, которые я хотел бы работать для производных классов базового класса 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 ]

6 голосов
/ 10 июля 2019

Во-первых, вам нужна черта, чтобы увидеть, что-то похоже на 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&);

Обратите внимание, что я просто беру здесь T и U, мы не обязательно хотим унывать и терять информацию.

<ч />

Одна из приятных особенностей концепций, появившихся в 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&);

В правилах языка уже предусмотрено, что перегрузка «оба А» предпочтительна, когда она жизнеспособна. Хорошая вещь.

2 голосов
/ 10 июля 2019

Учитывая, что в вашем примере первый элемент второй функции и второй элемент третьей не должны наследоваться от 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, если T и U оба наследуют первый, будет вызван, разве это не ожидаемое поведение?

Играть с кодом на: https://godbolt.org/z/ZA8hZz

Редактировать: для более общего ответа, пожалуйста, обратитесь к ответу Барри

0 голосов
/ 10 июля 2019

Это одна из тех ситуаций, когда удобно создать вспомогательный класс, который может быть частично специализирован для этого, с функцией, превращенной в оболочку, которая выбирает соответствующую специализацию:

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

Output (gcc 9):

LT, RT
L, RT
LT, R

Я ожидаю, что современные компиляторы C ++ потребуют выбрать только самый скромный уровень оптимизации, чтобы полностью оптимизировать вызов функции переноса.

...