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

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

#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 |
+---+-------------------------------+--------------------+

Понятно, что вычет на «б» не работает. Это два разных типа, поэтому вы не можете вывести T к ним обоим.

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

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


person Barry    schedule 13.07.2015    source источник
comment
Думаю, stackoverflow.com/a/1182688/4326278 содержит соответствующую информацию.   -  person bogdan    schedule 14.07.2015
comment
@bogdan Это имеет смысл с точки зрения отношения к typename identity<UniqueB>::type как к UniqueB_2. Но почему вычет при успешном? Я немного переформулировал вопрос.   -  person Barry    schedule 16.07.2015
comment
Насколько я могу судить, это применимо не только к генерации уникальных аргументов, но и к настройке параметров: T, typename identity<T>::type становится T, U, поэтому дедукция проходит успешно. Однако алгоритм явно выполняет еще несколько вещей, которые также не указаны в стандарте. В этом случае он «запоминает», что T фактически используется в конструкции, которая U заменена, так что в конце процесса дедукции, если T не был выведен откуда-то еще, дедукция по-прежнему не выполняется (есть подсказка на это в [14.8.2.4p11]).   -  person bogdan    schedule 16.07.2015
comment
Вот почему дедукция в примере из другого вопроса не выполняется, при использовании только одного параметра (bar(T) против bar(typename identity<T>::type)). Как я уже сказал, все это плохо определено. Проблема 1391, вероятно, изменит обработку невыведенные контексты и уточняйте детали, хотя все еще далеки от совершенства.   -  person bogdan    schedule 16.07.2015
comment
Кстати, еще один случай, который не указан, - это то, почему template<typename T> void f(T, T) более специализирован, чем template<typename T, typename U> void f(T, U). В стандарте нигде не говорится, что выводы для разных пар типов должны быть последовательными при частичном упорядочивании; указанный алгоритм имеет дело только с парами типов по отдельности. Моя версия: для параметров, для которых вызов функции имеет аргументы, вывод должен быть успешным для вызова синтезированной функции с использованием придуманных аргументов из A против объявления синтезированного шаблона с использованием параметров из P.   -  person bogdan    schedule 16.07.2015
comment
@bogdan Да, добавление Если конкретный P не содержит параметров шаблона, которые участвуют в выводе аргументов шаблона, этот P не используется для определения порядка. прояснил бы это. Однако это не решило бы проблему typename identity<UniqueB>::type --> UniqueB_2. Хотите объединить все это в ответ?   -  person Barry    schedule 16.07.2015
comment
@bogdan Почему не указано? Преобразование «первое ко второму» выполняется успешно (T == U == UniqueA), вывод «последнее к первому» терпит неудачу (вывод T,T против UniqueB, UniqueC).   -  person Barry    schedule 16.07.2015
comment
Где говорится, в алгоритме частичного упорядочивания, что выводы для разных пар типов должны быть согласованными? Он только говорит, что дедукция должна быть успешной для каждой пары, и в данном случае это так. Просто два типа, выведенные для T, будут разными.   -  person bogdan    schedule 16.07.2015
comment
@bogdan Я понимаю, о чем вы говорите. В основном нужно что-то вроде [temp.deduct.type] / 2 для случая частичного упорядочивания. Черт.   -  person Barry    schedule 16.07.2015
comment
Или просто сформулируйте начало алгоритма в терминах вывода из вызова функции, как я сделал в своей версии (два комментария выше). Что касается typename identity<UniqueB>::type --> UniqueB_2, это также должно быть решено с помощью того, что P не используется для определения порядка. Если я правильно понимаю, это должно означать , что пара P / A не используется (я не понимаю, как это имело бы смысл в противном случае), и в этом случае вы не получите единственный тип для удержания наоборот.   -  person bogdan    schedule 16.07.2015
comment
Собственно, сотрите вторую часть моего предыдущего комментария. Думаю, P не используется могло означать именно это. Использование имен в [14.8.2.4p10] может означать, что, если F имеет такой параметр, а G нет, будет меньше пар параметров, участвующих в определении того, является ли G по крайней мере таким же специализированным, как F, чем в противном случае вокруг. Мне придется подумать об этом еще немного, чтобы увидеть, как это повлияет на некоторые примеры. В любом случае, если никто не даст авторитетного ответа, попробую написать, скажем, завтра?   -  person bogdan    schedule 16.07.2015
comment
Простите, я сказал, что напишу ответ завтра; очевидно, что этого не произошло, и, в любом случае, я не уверен, что это принесет что-то полезное. Что действительно нужно, так это ответ из авторитетного источника и четкий текст в стандарте. Думаю, пока воздержусь от написания ответа.   -  person bogdan    schedule 21.07.2015
comment
Предлагаемое решение open-std.org/jtc1/ sc22 / wg21 / docs / cwg_active.html # 1391 может решить эту проблему.   -  person T.C.    schedule 27.07.2015
comment
@ T.C. Да, похоже, будет. Изменение на temp.deduct.partial исключит параметр typename identity<T>::type из рассмотрения.   -  person Barry    schedule 27.07.2015
comment
@ T.C. На прошлой неделе мы обсудили 1391 в комментариях и в чате, и, подумав еще немного, я составил список (что я думаю) проблем в предлагаемом решении. Это в моем ответе ниже; Мне бы очень хотелось знать, что вы думаете, ребята.   -  person bogdan    schedule 31.07.2015


Ответы (2)


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

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

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


Во-первых, предлагаемое решение для проблемы 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 отклоняет вызов как неоднозначный; не знаю почему.


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

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

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

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

person bogdan    schedule 30.07.2015
comment
Отлично. Как всегда. +1 - person Columbo; 15.08.2015
comment
Мне просто интересно, что я проголосовал за этот ответ, но так и не принял его. Уупс? В любом случае, если вы рядом, у меня есть еще один. - person Barry; 23.02.2017
comment
@bogdan, ваш ответ очень хорош, но третий пункт, который вы добавили, не является строгим. Возможно, вы неправильно поняли, например void test(T,typename U::b). Означает ли это, что просто T выведен успешно, тогда определение будет успешно (кроме тех, которые появляются только в невыведенных контекстах)? В соответствии с контекстом, который вы сказали, и протестируйте некоторые коды с помощью gcc, я думаю, что вы действительно хотите выразить, что ... если и только если параметры шаблона только появляются в невыведенных контекстах, дедукция будет поражением, не так ли? - person xmh0511; 13.02.2020
comment
@jackX Этот маркер должен быть взят вместе со вторым маркером из измененного [p10] ниже (все параметры, используемые в типах в G, должны иметь значения). Другими словами, маркеры из первого абзаца определяют условия для P/A пар, взятых по отдельности, а маркеры в [p10] определяют условия, которые применяются глобально, принимая шаблон G в целом. Это ответ на ваш вопрос? Если вы хотите обсудить свои примеры, я думаю, было бы неплохо открыть чат и ссылку на него отсюда. - person bogdan; 13.02.2020
comment
@bogdan, спасибо за ваш ответ. Короче говоря, если и только если параметры шаблона появляются только в невыведенных контекстах, вывод будет ошибочным, соответствующий пример - это 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"; }, Правильно ли я понимаю? - person xmh0511; 13.02.2020
comment
@jackX Я не согласен с единственным условием, если; Есть много случаев, когда параметры шаблона появляются в выводимых контекстах или одновременно выводятся и не выводятся, и вывод по-прежнему терпит неудачу. В противном случае, да, в вашем примере выведение с № 1 в качестве аргумента и № 2 в качестве параметра не выполняется, потому что в самом конце T остается без значения. Однако вычитание на уровне отдельных P/A пар считается успешным для второй пары, а typename A<T>::a = P. В общем (не в этом примере) это позволяет потенциально вывести T из другой пары P/A и получить значение в конце. - person bogdan; 13.02.2020
comment
@bogdan Итак, позвольте мне резюмировать наши комментарии для одной пары P/A, если P является невыведенным контекстом, независимо от того, какие параметры шаблона присутствуют только в нем или не только появляются в нем, это всегда можно вывести (другими словами , для любого невыведенного контекста это может быть выведено), затем в соответствии с [p50], который вы изменили, если какие-либо параметры шаблона остаются без значений, тогда результатом шаблона G будет выведено поражение, вы согласны со мной - person xmh0511; 14.02.2020
comment
@bogdan и еще один вопрос в том, почему третья метка кроме тех, которые появляются только ... а не тех, которые появляются в невыведенном контексте, поскольку третью пулю нужно рассматривать вместе с [p10], которую вы изменили, зачем подчеркивать единственное? - person xmh0511; 14.02.2020
comment
@jackX На ваш первый вопрос, я думаю, что да, если я правильно понял его суть. Я бы не сказал, что для любого невыведенного контекста это можно вывести, это немного противоречиво; для пары P/A вывод считается успешным, даже если некоторые параметры, появляющиеся в P, могут оставаться без значения, потому что они появляются только в невыведенных контекстах внутри этого P. Причина этого явного правила заключается в том, что формулировка использует этот успешный / неудачный результат для каждой отдельной пары, что отличается от обычного вывода ([temp.deduct.type] и другие), где ... - person bogdan; 14.02.2020
comment
... невыведенные контексты просто не участвуют в дедукции, и нас волнует только конечный общий результат. [p10] затем идет в конце и говорит именно это. Что касается вашего второго вопроса, то единственная причина состоит в том, что параметр может появляться как в выведенном, так и в невыведенном контексте в одном и том же P; например, B<T, typename A<T>::a>. - person bogdan; 14.02.2020
comment
Позвольте нам продолжить это обсуждение в чате. - person bogdan; 14.02.2020
comment
@bogdan Несколько новых вопросов в чате - person xmh0511; 15.02.2020

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

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

person Richard Corden    schedule 22.07.2015
comment
Значит, невыведенные контексты в аргументах полностью пропущены в (6)? - person Barry; 22.07.2015
comment
@Barry: определение типа определяет замену параметров шаблона с некоторой проверкой (например, типы обычно должны совпадать). Если все параметры имеют значения, они подставляются в тип, чтобы проверить, является ли последний тип «допустимым». Невыведенные контексты не влияют на значение параметров шаблона, но они используются при проверке типа. Дело в том, что я не уверен, что вы когда-нибудь сможете получить синтезированный тип, приводящий к недопустимому типу, т.е. что A<T>::type допустимо, но A<Q>::type недействительно. - person Richard Corden; 22.07.2015
comment
Но нам нужно определить, что типы совпадают в (6), поэтому мы знаем, что вывод b не удался. Если невыведенные контексты все еще используются при проверке типа, как это не потерпеть неудачу? Вы бы не проверяли identity<UniqueA>::type? - person Barry; 22.07.2015
comment
@Barry: [Тот же комментарий - незначительное изменение] Вывод не выполняется, потому что T сначала выводит Q1, а затем identity<Q1>::type. Они не считаются одним и тем же типом. В противном случае T приводит к Q2, а поскольку identity<T>::type - невыведенный контекст, дедукция не выполняется. У нас осталось единственное допустимое значение для T, поэтому вычет успешен. - person Richard Corden; 23.07.2015
comment
Выведение типа не выполняет проверку преобразований. - Не преобразований, а совместимость в смысле [14.8.2.5p1], который является относительно нечеткое понятие, но, насколько я могу судить, можно в общих чертах понимать как ту же форму, что подробно описана ниже, то есть в тексте [14.8.2.5]. Не будем забывать, что мы говорим конкретно о [14.8.2.4] (вывод при частичном упорядочивании), а не о каком-либо другом разделе. В этом контексте алгоритм явно выполняет больше проверок, чем только успешное определение типа, как указано в вашем маркере (6). - person bogdan; 23.07.2015
comment
Например, если ваше утверждение было верным, почему template<typename T> void f(T, int) более специализировано, чем template<typename T, typename U> void f(T, U)? Для второго параметра вычитание от int до U, очевидно, будет успешным, тогда, по вашей логике, наоборот, от Q2 (синтезировано для U) до int, никакого вычета не происходит. Как вы объяснили выше, у нас осталось одно допустимое значение для T, поэтому вывод будет успешным. Каждая перегрузка, по крайней мере, так же специализирована, как и другая, поэтому ни одна из них не может быть выбрана как более специализированная. - person bogdan; 23.07.2015
comment
@bogdan: Пункт об одном значении для T применяется только потому, что есть только один параметр шаблона T. Удержание будет успешным только в том случае, если все параметры шаблона явно указаны, имеют значения по умолчанию или выведены. Если U не выводится, вывод не выполняется. [Сказав это, я только что прочитал 14.8.2.4/11, что немного запутывает это] - person Richard Corden; 23.07.2015
comment
Почему U не может быть выведен? Я говорю о дедукции наоборот, с T, int в качестве параметров и Q1, Q2 (синтезированным из T, U) в качестве аргументов. По вашей логике этот вывод должен быть успешным, но на самом деле этого не происходит при частичном упорядочивании. - person bogdan; 23.07.2015
comment
@bogdan: Вы правы, мой предыдущий комментарий не исчерпывающий, особенно учитывая 14.8.2.4/11. В случае T, int я считаю, что разница в том, что int не является невыведенным контекстом (14.8.2.5/5). Вывод типа сравнивает синтезированный тип Q2 с int. Они не совпадают, и дедукция не выполняется. - person Richard Corden; 23.07.2015
comment
Позвольте нам продолжить это обсуждение в чате. - person Richard Corden; 23.07.2015
comment
@RichardCorden Я как-то совсем пропустил комментарий в чате! Это замечательная вещь, которую стоит прочитать, спасибо. - person Barry; 31.07.2015