SFINAE работает с вычетом, но не с заменой

Рассмотрим следующий 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 не должен работать во время замены? Тем не менее, здесь, кажется, работает только во время удержания.

Ответы

Ответ 1

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

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

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

test(a, b);     // works

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

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

Легко. 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 Теперь подставим - подпись есть (A, A). Отлично.

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

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

using type = typename T::type;

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

Вернуться к LF

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

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

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

Отсутствующий type приводит к серьезной ошибке. Прочитайте fooobar.com/questions/175524/.... По сути, у нас есть два этапа при определении того, является ли template<class T> void test(Wrapper<T>, Wrapper<T>) желаемой перегрузкой:

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

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

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

Ответ 2

Я не адвокат по языку, но я не думаю, что определение 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.

-- РЕДАКТИРОВАТЬ --

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

Мой 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.

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

template <typename T>
std::enable_if_t<!is_wrapper<T>{}> test(T, T)
 { }

Ответ 3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 4

SFINAE применяется только при вызове перегрузки, установленной по имени:

[temp.over] (акцент мой)

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

Когда вы пишете test(a, b) это вызов этого имени. Когда вы пишете test<A>(a, b) он больше не использует имя, а простой идентификатор шаблона, обозначающий специализацию.

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