Подтвердить что ты не робот

Частичный порядок шаблонов - почему частичный вывод преуспевает здесь

Рассмотрим следующие простые (в той мере, в какой это могут быть вопросы шаблона):

#include <iostream>

template <typename T>
struct identity;

template <>
struct identity<int> {
    using type = int;
};

template<typename T> void bar(T, T ) { std::cout << "a\n"; }
template<typename T> void bar(T, typename identity<T>::type) { std::cout << "b\n"; }

int main ()
{
    bar(0, 0);
}

Оба clang и gcc печатают "a". Согласно правилам [temp.deduct.partial] и [temp.func.order], для определения частичного упорядочения нам нужно синтезировать некоторые уникальные типы. Итак, у нас есть две попытки вычета:

+---+-------------------------------+-------------------------------------------+
|   | Parameters                    | Arguments                                 |
+---+-------------------------------+-------------------------------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA                          |
| b | T, T                          | UniqueB, typename identity<UniqueB>::type |
+---+-------------------------------+-------------------------------------------+

Для вывода на "b", согласно ответа Ричарда Кордена, выражение typename identity<UniqueB>::type рассматривается как тип и не оценивается. То есть, это будет синтезировано так:

+---+-------------------------------+--------------------+
|   | Parameters                    | Arguments          |
+---+-------------------------------+--------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA   |
| b | T, T                          | UniqueB, UniqueB_2 |
+---+-------------------------------+--------------------+

Ясно, что вывод на "b" терпит неудачу. Это два разных типа, поэтому вы не можете вывести T для обоих из них.

Однако мне кажется, что вывод на A должен завершиться неудачей. Для первого аргумента вы должны соответствовать T == UniqueA. Второй аргумент - это не выведенный контекст - так что не удалось бы сделать вывод, если if UniqueA были преобразованы в identity<UniqueA>::type? Последнее является неудачей замещения, поэтому я не вижу, как это удержание может быть успешным.

Как и почему gcc и clang предпочитают "a" перегрузку в этом сценарии?

4b9b3361

Ответ 1

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

Чтобы сделать вещи еще более интересными, MSVC (я тестировал 12 и 14) отклонил вызов как неоднозначный. Я не думаю, что в стандарте нет ничего, чтобы окончательно доказать, какой компилятор прав, но я думаю, что я мог бы понять, откуда эта разница; есть примечание об этом ниже.

Ваш вопрос (и этот) заставил меня сделать еще одно исследование того, как все работает. Я решил написать этот ответ не потому, что считаю его авторитетным, а скорее организовывать информацию, которую я нашел в одном месте (это не соответствовало комментариям). Надеюсь, это будет полезно.


Во-первых, предлагаемая резолюция для issue 1391. Мы подробно обсуждали его в комментариях и чатах. Я думаю, что, хотя он и дает некоторые разъяснения, он также вводит некоторые проблемы. Он изменяет [14.8.2.4p4] на (новый текст выделен жирным шрифтом):

Каждый тип, назначенный выше из шаблона параметра и соответствующий тип из шаблона аргумента используется как типы P и A. Если конкретный P не содержит шаблонных параметров, которые участвовать в выводе аргумента шаблона, что P не используется для определить порядок.

Не очень хорошая идея, на мой взгляд, по нескольким причинам:

  • Если P не является зависимым, он не содержит никаких параметров шаблона вообще, поэтому он не содержит никого, участвующего в выводе аргумента, что сделало бы смелое выражение применительно к нему. Тем не менее, это сделало бы template<class T> f(T, int) и template<class T, class U> f(T, U) неупорядоченными, что не имеет смысла. Это, вероятно, вопрос толкования формулировки, но это может вызвать путаницу.
  • Это противоречит понятию используемого для определения порядка, которое влияет на [14.8.2.4p11]. Это делает ошибки template<class T> void f(T) и template<class T> void f(typename A<T>::a) неупорядоченными (вывод выполняется с первого по второй, поскольку T не используется в типе, используемом для частичного упорядочения в соответствии с новым правилом, поэтому он может оставаться без значения). В настоящее время все компиляторы, которые я тестировал, считают второй более специализированным.
  • В следующем примере #2 будет более специализированным, чем #1:

    #include <iostream>
    
    template<class T> struct A { using a = T; };
    
    struct D { };
    template<class T> struct B { B() = default; B(D) { } };
    template<class T> struct C { C() = default; C(D) { } };
    
    template<class T> void f(T, B<T>) { std::cout << "#1\n"; } // #1
    template<class T> void f(T, C<typename A<T>::a>) { std::cout << "#2\n"; } // #2
    
    int main()
    {
       f<int>(1, D());
    }
    

    (#2 второй параметр не используется для частичного упорядочения, поэтому вывод удаляется от #1 до #2, но не наоборот). В настоящее время вызов неоднозначен и, вероятно, останется таким.


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

Оставьте [p4] как есть и добавьте следующее между [p8] и [p9]:

Для пары P/A:

  • Если P не зависит, вывод считается успешным тогда и только тогда, когда P и A являются одним и тем же типом.
  • Замещение выведенных параметров шаблона в невыводимые контексты, появляющиеся в P, не выполняется и не влияет на результат процесса дедукции.
  • Если значения аргументов шаблона успешно выведены для всех параметров шаблона P, кроме тех, которые отображаются только в невыводимых контекстах, то вывод считается успешным (даже если некоторые параметры, используемые в P, остаются без значения при конец процесса дедукции для этой пары P/A).

Примечания:

  • О второй маркерной точке: [14.8.2.5p1] говорит о поиске значений аргументов шаблона, которые сделают P, после подстановки выведенных значений (назовите его выведенным A), совместимым с A. Это может вызвать путаницу в том, что на самом деле происходит при частичном заказе; нет никакой замены.
  • В некоторых случаях MSVC, похоже, не реализует третий маркер. Подробнее см. Следующий раздел.
  • Вторая и третья пулевые точки предназначены также для случаев, когда P имеет формы типа A<T, typename U::b>, которые не охватываются формулировкой в ​​номере 1391.

Измените текущий [p10] на:

Шаблон функции F по меньшей мере такой же специализированный, как шаблон функции G тогда и только тогда, когда:

  • для каждой пары типов, используемых для определения порядка, тип из F по меньшей мере такой же специализированный, как тип из G, и
  • при выполнении вычета с использованием преобразованного F в качестве шаблона аргумента и G в качестве шаблона параметра после завершения вычитания для всех пар типов все параметры шаблона, используемые в типах из G, которые используются для определения порядка, имеют значения, а те значения соответствуют всем парам типов.

F более специализирован, чем G, если F по крайней мере, как специализированный поскольку G и G не являются, по меньшей мере, такими же специализированными, как F.

Сделайте весь текущий [p11] заметкой.

(примечание, добавленное в резолюции от 1391 до [14.8.2.5p4], должно быть скорректировано также - оно отлично подходит для [14.8.2.1], но не для [14.8.2.4].)


Для MSVC в некоторых случаях все параметры шаблона в P должны получать значения во время вычитания для этой конкретной пары P/A для того, чтобы вычет преуспел от A до P. Я думаю, что это может быть причиной возникновения расхождения в вашем примере и других, но я видел, по крайней мере, один случай, когда вышеприведенное не применяется, поэтому я не уверен, чему верить.

Другой пример, в котором, как представляется, применяется вышеприведенное выражение: изменение template<typename T> void bar(T, T) до template<typename T, typename U> void bar(T, U) в ваших примерах свопирует результаты вокруг: вызов неоднозначен в Clang и GCC, но разрешается b в MSVC.

В одном примере, где это не так:

#include <iostream>

template<class T> struct A { using a = T; };
template<class, class> struct B { };

template<class T, class U> void f(B<U, T>) { std::cout << "#1\n"; }
template<class T, class U> void f(B<U, typename A<T>::a>) { std::cout << "#2\n"; }

int main()
{
   f<int>(B<int, int>());
}

Это выбирает #2 в Clang и GCC, как и ожидалось, но MSVC отклоняет вызов как неоднозначный; не знаю, почему.


Алгоритм частичного упорядочения, описанный в стандарте, говорит о синтезе уникального типа, значения или шаблона класса для генерации аргументов. Клэнг управляет этим... не синтезируя ничего. Он просто использует оригинальные формы зависимых типов (как заявлено) и соответствует им в обоих направлениях. Это имеет смысл, поскольку замена синтезированных типов не добавляет никакой новой информации. Он не может изменять формы типов A, поскольку вообще невозможно определить, какие конкретные типы могут быть заменены замещаемыми формами. Синтезированные типы неизвестны, что делает их очень похожими на параметры шаблона.

При столкновении с P, который является не выводимым контекстом, алгоритм дедукции аргумента шаблона Клана просто пропускает его, возвращая "успех" для этого конкретного шага. Это происходит не только во время частичного упорядочения, но и для всех типов вычетов, а не только на верхнем уровне в списке параметров функции, но рекурсивно всякий раз, когда невыводимый контекст встречается в виде составного типа. По какой-то причине я обнаружил, что это удивительно в первый раз, когда я это увидел. Думая об этом, оно, конечно, имеет смысл и соответствует стандарту ([...] не участвует в выводе типа [...] в [14.8.2.5p4]).

Это согласуется с комментариями Ричарда Кордена к его ответу, но мне нужно было увидеть компилятор кода, чтобы понять все последствия (не по вине его ответа, а скорее из моего собственного - программиста, думающего в коде и всего этого).

Я добавил дополнительную информацию о реализации Clang в этом ответе.

Ответ 2

Я считаю, что ключ со следующим утверждением:

Второй аргумент - это не выведенный контекст - так что не будет ли этот вывод успешным, если UniqueA были конвертированы в identity:: type?

Вычисление типа не выполняет проверку "конверсий". Эти проверки выполняются с использованием реальных явных и выведенных аргументов как часть разрешения перегрузки.

Это мое резюме шагов, которые предпринимаются для выбора шаблона функции для вызова (все ссылки взяты из N3937, ~ С++ '14):

  • Явные аргументы заменяются, и результирующий тип функции проверяет, что он действителен. (14.8.2/2)
  • Выполняется вывод типа и результирующие выведенные аргументы заменяются. Опять же, результирующий тип должен быть действительным. (14.8.2/5)
  • Шаблоны функций, которые успешно выполняются на шагах 1 и 2, являются специализированными и включены в набор перегрузки для разрешения перегрузки. (14.8.3/1)
  • Последовательности преобразования сравниваются с помощью разрешения перегрузки. (13.3.3)
  • Если последовательности преобразования двух специализированных функций не являются "лучшими", алгоритм частичного упорядочения используется для поиска более специализированного шаблона функции. (13.3.3)
  • Алгоритм частичного упорядочения проверяет только, что вывод типа преуспевает. (14.5.6.2/2)

Компилятор уже знает к шагу 4, что обе специализации могут вызываться при использовании реальных аргументов. Шаги 5 и 6 используются для определения того, какая из функций более специализирована.