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

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

#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» в этом сценарии?

19

Решение

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

Чтобы сделать вещи еще более интересными, 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]).

Это согласуется с Ричард Корден комментарии к его ответ, но я должен был на самом деле увидеть код компилятора, чтобы понять все последствия (не ошибка его ответа, а скорее мой собственный — мыслитель программиста в коде и все такое).

Я включил немного больше информации о реализации Clang в этот ответ.

22

Другие решения

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

Второй аргумент — это не выводимый контекст — так что, если бы 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 используются для определения того, какая из функций является более специализированной.

5

По вопросам рекламы [email protected]