Почему Разрешение перегрузки предпочитает неограниченную функцию шаблона по сравнению с более конкретной?

У меня есть эта минимальная библиотека шаблонов выражений с умножением, т.е.

template <typename T, typename U>
struct mul {
    const T &v1;
    const U &v2;
};

template <typename T, typename U>
mul<T, U> operator*(const T &one, const U &two) {
    std::cout << " called: mul<T, U> operator*(const T &one, const T &two)\n";
    return mul<T, U>{one, two};
}

и транспонировать, т.е.

template <typename T>
struct transpose {
    const T &t;
};

template <typename T>
transpose<T> tran(const T &one) {
    return transpose<T>{one};
}

Я представлю некоторые типы A и B, где последний является подклассом первого:

template <typename T>
struct A {
    T elem;
};

template <typename T>
struct B : A<T> {
    B(T val) : A<T>{val} {}
};

Затем я могу вызвать библиотеку шаблонов выражений следующим образом (с перегрузкой для печати в std::cout):

template <typename T, typename U>
std::ostream &operator<<(std::ostream &os, const mul<T, U> &m) {
    os << " unconstrained template \n";
}

int main(int argc, char const *argv[]) {
    B<double> a{2};
    B<double> b{3};
    std::cout << tran(a) * b << "\n";
    return 0;
}

Это дает мне вывод:

called: mul<T, U> operator*(const T &one, const T &two)
unconstrained template 

Все идет нормально. Предположим теперь, что я хочу специализированный режим для "транспонирования переменной типа A<T> умноженной на переменную типа A<T> для некоторого типа T ", как я делал в своей main. С этой целью я представлю

template <typename T>
T operator*(const transpose<A<T>> &one, const A<T> &two) {
    std::cout << " called: T operator*(const A<T> &one, const A<T> &two)\n";
    return one.t.elem * two.elem;
}

Я запускаю ту же main функцию, что и выше, и все равно получаю тот же вывод, что и выше (unconstrained template). Этого и следовало ожидать, поскольку transpose<B<double>> - это совершенно другой тип по сравнению с transpose<A<double>>, поэтому при разрешении перегрузки выбирается неограниченная версия шаблона operator*.

(Конечно, если я изменю свои определения переменных в main на A вместо B, ADL вызовет специализированную функцию и будет called: T operator*(const A<T> &one, const A<T> &two) выход called: T operator*(const A<T> &one, const A<T> &two) и 6).

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

template <typename T, typename V>
std::enable_if_t<std::is_base_of<A<T>, V>::value, T> operator*(const transpose<V> &one,
                                                               const V &two) {
    std::cout << " called: std::enable_if_t<std::is_base_of<A<T>, V>::value, T> operator*(const "
                 "transpose<V> &one, const V &two)\n";
    return one.t.elem * two.elem;
}

Даже используя operator* SFINAE operator* я все равно получаю unconstrained template версию unconstrained template. Как так? Какие изменения я должен сделать, чтобы вызвать более специализированную функцию шаблона?

Ответ 1

Проблема состоит в том, что в перегрузке SFINAE T используется в недедуцированном контексте. Вы фактически спрашиваете компилятор: "Включите это, если существует T такой, что A<T> является базовым классом V ". Экзистенциальная количественная оценка является хорошим показателем того, что то, что вы запрашиваете, не может быть SFINAEd.

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

Вы можете решить эту проблему, сделав T доступным через ваши классы A (и, следовательно, B), например:

template <typename T>
struct A {
    using Type = T;
    T elem;
};


template <typename V>
std::enable_if_t<std::is_base_of<A<typename V::Type>, V>::value, typename V::Type> operator*(const transpose<V> &one,
                                                               const V &two) {
    std::cout << " called: std::enable_if_t<std::is_base_of<A<T>, V>::value, T> operator*(const "
                 "transpose<V> &one, const V &two)\n";
    return one.t.elem * two.elem;
}

[Живой пример]