SFINAE работает с вычетом, но не с заменой - PullRequest
21 голосов
/ 30 апреля 2019

Рассмотрим следующую MCVE

struct A {};

template<class T>
void test(T, T) {
}

template<class T>
class Wrapper {
    using type = typename T::type;
};

template<class T>
void test(Wrapper<T>, Wrapper<T>) {
}

int main() {
    A a, b;
    test(a, b);     // works
    test<A>(a, b);  // doesn't work
    return 0;
}

Здесь test(a, b); работает, а test<A>(a, b); не работает с:

<source>:11:30: error: no type named 'type' in 'A'
    using type = typename T::type;
                 ~~~~~~~~~~~~^~~~
<source>:23:13: note: in instantiation of template class 'Wrap<A>' requested here
    test<A>(a, b);  // doesn't work
            ^
<source>:23:5: note: while substituting deduced template arguments into function template 'test' [with T = A]
    test<A>(a, b);  // doesn't work

LIVE DEMO

Вопрос: Почему это так? Разве SFINAE не должен работать во время замены ? И все же здесь, похоже, он работает только при удержании .

Ответы [ 3 ]

20 голосов
/ 30 апреля 2019

Самостоятельное введение

Привет всем, я невинный компилятор.

Первый звонок

test(a, b);     // works

В этом вызове тип аргумента A. Позвольте мне сначала рассмотреть первую перегрузку:

template <class T>
void test(T, T);

Easy. T = A. Теперь рассмотрим второе:

template <class T>
void test(Wrapper<T>, Wrapper<T>);

Хм ... что? Wrapper<T> для A? Мне нужно создать экземпляр Wrapper<T> для каждого возможного типа T в мире, чтобы убедиться, что параметр типа Wrapper<T>, который может быть специализированным, не может быть инициализирован с аргументом типа A? Ну ... я не думаю, что я собираюсь это сделать ...

Следовательно, я не буду создавать никаких экземпляров Wrapper<T>. Я выберу первую перегрузку.

Второй звонок

test<A>(a, b);  // doesn't work

test<A>? Ага, мне не нужно делать дедукцию. Позвольте мне проверить две перегрузки.

template <class T>
void test(T, T);

T = A. Теперь замените & mdash; подпись (A, A). Совершенная.

template <class T>
void test(Wrapper<T>, Wrapper<T>);

T = A. Теперь субт ... Подожди, я никогда не создавал Wrapper<A>? Я не могу заменить тогда. Как я могу узнать, будет ли это жизнеспособной перегрузкой для вызова? Ну, я должен сначала создать его экземпляр. (создание экземпляра) Подождите ...

using type = typename T::type;

A::type? Ошибка!

Вернуться к Л.Ф.

Привет всем, я Л.Ф. Давайте рассмотрим, что сделал компилятор.

Был ли компилятор достаточно невинным? Соответствовал ли он (она?) Стандарту? @ YSC указал, что [temp.over] / 1 говорит:

Когда пишется вызов функции или шаблона функции (явно или неявно используя операторную нотацию), шаблон удержание аргумента ([temp.deduct]) и проверка любого явного аргументы шаблона ([temp.arg]) выполняются для каждой функции шаблон, чтобы найти значения аргумента шаблона (если есть), которые могут быть используется с этим шаблоном функции для создания экземпляра шаблона функции специализация, которая может быть вызвана с помощью аргументов вызова. Для каждого шаблон функции, , если вывод аргумента и проверка успешны , аргументы шаблона (выведенные и / или явные) используются для синтезировать объявление одного шаблона функции специализация, которая добавляется к функциям-кандидатам используется в разрешении перегрузки. Если для данного шаблона функции, сбой вывода аргумента или шаблон синтезированной функции специализация будет плохо сформирована, такая функция не будет добавлена ​​к набор функций-кандидатов для этого шаблона. полный набор Функции-кандидаты включают в себя все синтезированные объявления и все не шаблонных перегруженных функций с тем же именем. Синтезированные объявления обрабатываются как любые другие функции в остаток разрешения перегрузки, за исключением случаев, явно указанных в [Over.match.best].

Отсутствие type приводит к серьезной ошибке. Считайте https://stackoverflow.com/a/15261234. По сути, у нас есть два этапа, чтобы определить, является ли template<class T> void test(Wrapper<T>, Wrapper<T>) желаемой перегрузкой:

  1. Инстанцирование. В этом случае мы (полностью) создаем экземпляр Wrapper<A>. На этом этапе using type = typename T::type; проблематично, поскольку A::type не существует. Проблемы, возникающие на этом этапе, являются серьезными ошибками.

  2. Замена. Так как первая стадия уже терпит неудачу, эта стадия даже не достигнута в этом случае. Проблемы, возникающие на этом этапе, регулируются SFINAE.

Так что да, невинный компилятор поступил правильно.

4 голосов
/ 30 апреля 2019

Я не адвокат по языку, но я не думаю, что определение using type = typename T::type; внутри класса само по себе может использоваться как SFINAE для включения / отключения функции, получающей объект этого класса.

Если вы хотите решение, вы можете применить SFINAE к версии Wrapper следующим образом

template<class T>
auto test(Wrapper<T>, Wrapper<T>)
   -> decltype( T::type, void() )
 { }

Таким образом, эта функция test() включена только для типов T с типом typeопределено внутри него.

В вашей версии включено для каждого типа T, но выдает ошибку, когда T несовместимо с Wrapper.

- EDIT -

ОП уточняет и спрашивает

У My Wrapper гораздо больше зависимостей от T, было бы нецелесообразно дублировать их все в выражении SFINAE.Разве нет способа проверить, может ли быть создан экземпляр самого Wrapper?

Как предлагает Холт, вы можете создать черты нестандартного типа, чтобы увидеть, является ли тип типом Wrapper<something>;Например,

template <typename>
struct is_wrapper : public std::false_type
 { };

template <typename T>
struct is_wrapper<Wrapper<T>> : public std::true_type
 { using type = T; };

Затем вы можете изменить версию Wrapper для получения типа U и проверить, является ли U тип Wrapper<something>

template <typename U>
std::enable_if_t<is_wrapper<U>{}> test (U, U)
 { using T = typename is_wrapper<U>::type; }

Обратите внимание, чтоВы можете восстановить исходный тип T (если он вам нужен), используя определение type внутри структуры is_wrapper.

Если вам нужна не Wrapper версия test(), сэто решение, вы должны отключить его, когда T тип Wrapper<something>, чтобы избежать столкновения

template <typename T>
std::enable_if_t<!is_wrapper<T>{}> test(T, T)
 { }
1 голос
/ 01 мая 2019

Удержание функции, вызываемой в выражении вызова функции, выполняется в два этапа:

  1. Определение набора жизнеспособных функций;
  2. Определение наилучшей жизнеспособной функции.

Набор жизнеспособных функций может содержать только объявление функции и объявление специализации шаблонной функции .

Таким образом, когда выражение вызова (test(a,b) или test<A>(a,b)) именует функцию шаблона, необходимо определить все аргументы шаблона: это называется вычетом аргумента шаблона. Это выполняется в три этапа [temp.deduct]:

  1. Подстановка явно предоставленных аргументов шаблона (в names<A>(x,y) A явно указывается) (подстановка означает, что в функции delcaration шаблона функции заменяется их аргумент)
  2. Удержание аргументов шаблона, которые не предоставлены;
  3. Подстановка выведенного аргумента шаблона.

Выражение вызова test(a,b)

  1. Нет явно предоставленного аргумента шаблона.
  2. T выводится на A для первой функции шаблона, вычет не выполняется для второй функции шаблона [temp.deduct.type] / 8 . Поэтому вторая функция шаблона не будет участвовать в разрешении перегрузки
  3. A заменяется в объявлении первой функции шаблона. Положение успешно.

Таким образом, в наборе имеется только одна перегрузка, и она выбирается разрешением перегрузки.

Выражение вызова test<A>(a,b)

(Изменить после соответствующих замечаний @ T.C. И @geza)

  1. Предоставляется аргумент шаблона: A, и он подставляется в объявлении двух функций шаблона. Эта замена включает только создание экземпляра объявления специализации шаблона функции. Так что это нормально для двух шаблонов
  2. Нет вычета аргумента шаблона
  3. Без замены выведенного аргумента шаблона.

Таким образом, две специализации шаблона, test<A>(A,A) и test<A>(Wrapper<A>,Wrapper<A>), участвуют в разрешении перегрузки. Сначала компилятор должен определить, какая функция является жизнеспособной. Для этого компилятору необходимо найти неявную последовательность преобразования, которая преобразует аргумент функции в тип параметра функции [over.match.viable] / 4 :

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

Для второй перегрузки, чтобы найти преобразование в Wrapper<A>, компилятору нужно определение этого класса. Так что это (неявно) создает его. Это тот экземпляр, который вызывает наблюдаемую ошибку, генерируемую компиляторами.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...