У меня есть эта библиотека шаблонов минимальных выражений с умножением, т.е.
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)
и 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;
}
Даже используя SFINAE'd operator*
, я все равно получаю unconstrained template
версию. Почему? Какие изменения я должен внести, чтобы вызвать более специализированную функцию шаблона?
Я думал, что «Разрешение перегрузки» и ADL — это одно и то же. Судя по всему, это не так.
Разрешение перегрузки - это выбор между найденными именами в т.ч. найденные ADL.
Проблема в том, что в перегрузке SFINAE T
используется в невыведенном контексте. Фактически вы просите компилятор: «Включите это, если существует T
такой, что A<T>
является базовым классом V
». Экзистенциальная квантификация — хороший показатель того, что то, о чем вы просите, не может быть подвергнуто SFINAE.
Вы можете увидеть это сами, если отключите неограниченный шаблон, как я сделал здесь. Это заставляет компилятор объяснять, почему другая функция недопустима.
Вы можете решить эту проблему, сделав 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;
}
На самом деле удаление sfinae, лишнего аргумента шаблона, а затем раскомментирование неограниченного аргумента дает мне правильный результат.
@GuillaumeRacicot Да, это работает в опубликованном сокращенном примере. Но обратите внимание, что в OP говорится, что версию с ограничениями следует использовать только тогда, когда V
получено из A<T>
для некоторых A
. Удаление sfinae сделает его применимым ко всем парам транспонирование-нетранспонирование.
Читая этот SFINAE, он должен позволять typename V
быть B<T>
, но, учитывая, что std::is_base_of<Base, Derived>::value
дает true
, если std::is_same<Base, Derived>::value
, он также позволяет A<T>
. Разве не должна быть проверка, запрещающая и это? Если я правильно понимаю намерение ОП, то есть.
@StackDanny Я понял вопрос ОП как «как мне назвать SFINAE для особого случая, который включает A<T>
или тип, производный от A<T>
?» Вот как я прочитал: «Предположим, теперь мне нужно специализированное лечение для «транспонирования переменной типа A<T>
, умноженной на переменную типа A<T>
для некоторого типа T
», как у меня было в моем main
». Обратите внимание, что на самом деле это была B<double>
в main
.
@Angew Я просто нахожу это странным, потому что, если вы удалите struct B
из кода, SFINAE (который строго запрашивает случай std::is_base_of
) все равно будет вызываться, даже если наследования не происходит. Я просто думаю, что это непреднамеренно. Я полагаю, в этом виновата реализация std::is_base_of
.
@Angew Вы правильно интерпретировали мое намерение, хотя в более общем смысле A<T>
был бы абстрактным классом.
@StackDanny std::is_base_of
эффективно моделирует IS-A. Справедливости ради, на практике гораздо чаще требуется знать «вы X или один из его потомков», чем «вы произошли от X, но не от самого X».
Может быть, мое понимание того, что такое ADL, неверно, но какое это имеет отношение к ADL?