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

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

#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):

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

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

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