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

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

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?

Evg 29.05.2019 14:38

Я думал, что «Разрешение перегрузки» и ADL — это одно и то же. Судя по всему, это не так.

Nibor 29.05.2019 15:10

Разрешение перегрузки - это выбор между найденными именами в т.ч. найденные ADL.

curiousguy 06.06.2019 07:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
14
3
195
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема в том, что в перегрузке 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, лишнего аргумента шаблона, а затем раскомментирование неограниченного аргумента дает мне правильный результат.

Guillaume Racicot 29.05.2019 14:35

@GuillaumeRacicot Да, это работает в опубликованном сокращенном примере. Но обратите внимание, что в OP говорится, что версию с ограничениями следует использовать только тогда, когда V получено из A<T> для некоторых A. Удаление sfinae сделает его применимым ко всем парам транспонирование-нетранспонирование.

Angew is no longer proud of SO 29.05.2019 14:37

Читая этот SFINAE, он должен позволять typename V быть B<T>, но, учитывая, что std::is_base_of<Base, Derived>::value дает true, если std::is_same<Base, Derived>::value, он также позволяет A<T>. Разве не должна быть проверка, запрещающая и это? Если я правильно понимаю намерение ОП, то есть.

Stack Danny 29.05.2019 16:00

@StackDanny Я понял вопрос ОП как «как мне назвать SFINAE для особого случая, который включает A<T> или тип, производный от A<T>?» Вот как я прочитал: «Предположим, теперь мне нужно специализированное лечение для «транспонирования переменной типа A<T>, умноженной на переменную типа A<T> для некоторого типа T», как у меня было в моем main». Обратите внимание, что на самом деле это была B<double> в main.

Angew is no longer proud of SO 29.05.2019 16:02

@Angew Я просто нахожу это странным, потому что, если вы удалите struct B из кода, SFINAE (который строго запрашивает случай std::is_base_of) все равно будет вызываться, даже если наследования не происходит. Я просто думаю, что это непреднамеренно. Я полагаю, в этом виновата реализация std::is_base_of.

Stack Danny 29.05.2019 16:13

@Angew Вы правильно интерпретировали мое намерение, хотя в более общем смысле A<T> был бы абстрактным классом.

Nibor 29.05.2019 16:32

@StackDanny std::is_base_of эффективно моделирует IS-A. Справедливости ради, на практике гораздо чаще требуется знать «вы X или один из его потомков», чем «вы произошли от X, но не от самого X».

Angew is no longer proud of SO 29.05.2019 16:47

Другие вопросы по теме