Частичное упорядочение шаблона - почему частичное удержание здесь успешно
Рассмотрим следующий простой (в той мере, в какой это касается шаблонных вопросов) пример:
#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 |
+---+-------------------------------+-------------------------------------------+
Для вычета на "б", согласно ответу Ричарда Кордена, выражение typename identity<UniqueB>::type
рассматривается как тип и не оценивается. То есть это будет синтезировано, как если бы это было:
+---+-------------------------------+--------------------+
| | Parameters | Arguments |
+---+-------------------------------+--------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA |
| b | T, T | UniqueB, UniqueB_2 |
+---+-------------------------------+--------------------+
Понятно, что вычет на "б" не удается. Это два разных типа, поэтому вы не можете вывести T
для них обоих.
Тем не менее, мне кажется, что вычет на A
должен потерпеть неудачу. Для первого аргумента, вы бы соответствовали T == UniqueA
, Второй аргумент - это не выводимый контекст, так что если бы этот вывод не был успешным, если бы UniqueA
были конвертируемы в identity<UniqueA>::type
? Последнее является ошибкой замещения, поэтому я не вижу, как этот вывод мог бы быть успешным.
Как и почему gcc и clang предпочитают перегрузку "a" в этом сценарии?
2 ответа
Как обсуждалось в комментариях, я полагаю, что есть несколько аспектов алгоритма частичного упорядочения шаблона функции, которые не ясны или вообще не указаны в стандарте, и это показано в вашем примере.
Чтобы сделать вещи еще более интересными, MSVC (я тестировал 12 и 14) отклоняет вызов как неоднозначный. Я не думаю, что в стандарте есть что-либо, чтобы окончательно доказать, какой компилятор прав, но я думаю, что мог бы понять, откуда взялась разница; ниже есть примечание об этом.
Ваш вопрос (и этот) заставил меня провести еще одно расследование того, как все работает. Я решил написать этот ответ не потому, что считаю его авторитетным, а скорее организовать информацию, которую я нашел в одном месте (она не помещалась бы в комментариях). Надеюсь это будет полезно.
Во-первых, предлагаемое решение для вопроса 1391. Мы много обсуждали это в комментариях и чате. Я думаю, что, хотя он и дает некоторые разъяснения, он также вносит некоторые проблемы. Изменяется [14.8.2.4p4] на (новый текст, выделенный жирным шрифтом):
Каждый тип, назначенный выше из шаблона параметра, и соответствующий тип из шаблона аргумента используются в качестве типов
P
а такжеA
, Если конкретныйP
не содержит шаблонных параметров, которые участвуют в выводе аргументов шаблона, чтоP
не используется для определения порядка.
На мой взгляд, не очень хорошая идея по нескольким причинам:
- Если
P
не зависит, он не содержит никаких параметров шаблона вообще, поэтому он также не содержит никаких параметров, которые участвуют в выводе аргумента, что могло бы применить к нему выражение bold. Тем не менее, это сделало бы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 отклоняет вызов как неоднозначный; понятия не имею почему.
Алгоритм частичного упорядочения, описанный в стандарте, говорит о синтезе уникального типа, значения или шаблона класса для генерации аргументов. Clang управляет этим, не синтезируя ничего. Он просто использует исходные формы зависимых типов (как объявлено) и сопоставляет их в обоих направлениях. Это имеет смысл, поскольку замена синтезированных типов не добавляет никакой новой информации. Это не может изменить формы A
типы, поскольку, как правило, невозможно определить, к каким конкретным типам могут обращаться замещенные формы. Синтезированные типы неизвестны, что делает их очень похожими на параметры шаблона.
При встрече с P
это не выводимый контекст, алгоритм вывода аргументов шаблона Clang просто пропускает его, возвращая "success" для этого конкретного шага. Это происходит не только во время частичного упорядочения, но и для всех типов вычетов, и не только на верхнем уровне в списке параметров функции, но и рекурсивно всякий раз, когда встречается невыгруженный контекст в форме составного типа. Почему-то я обнаружил это удивительным, когда впервые увидел это. Размышляя об этом, это, конечно, имеет смысл и соответствует стандарту ([...] не участвует в выводе типов [...] в [14.8.2.5p4]).
Это согласуется с комментариями Richard Corden к его ответу, но я должен был на самом деле увидеть код компилятора, чтобы понять все последствия (не ошибка его ответа, а скорее мое собственное - программистское мышление в коде и все такое).
В этот ответ я включил еще немного информации о реализации Clang.
Я считаю, что ключ к следующему утверждению:
Второй аргумент - это не выводимый контекст - так что, если бы UniqueA не был преобразован в identity::type, этот вывод не был бы успешным?
Тип удержания не выполняет проверку "преобразований". Эти проверки выполняются с использованием реальных явных и выводимых аргументов как часть разрешения перегрузки.
Вот мое резюме шагов, которые предпринимаются для выбора шаблона функции для вызова (все ссылки взяты из N3937, ~ C++ '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 используются для определения того, какая из функций является более специализированной.