Согласно [temp.class.order] §14.5.5.2, выбор частичной специализации t
в этом примере:
template< typename >
struct s { typedef void v, w; };
template< typename, typename = void >
struct t {};
template< typename c >
struct t< c, typename c::v > {};
template< typename c >
struct t< s< c >, typename s< c >::w > {};
t< s< int > > q;
эквивалентно выбору перегрузки f
в этом примере:
template< typename >
struct s { typedef void v, w; };
template< typename, typename = void >
struct t {};
template< typename c >
constexpr int f( t< c, typename c::v > ) { return 1; }
template< typename c >
constexpr int f( t< s< c >, typename s< c >::w > ) { return 2; }
static_assert ( f( t< s< int > >() ) == 2, "" );
Однако GCC, Clang и ICC все отклоняют первый пример как неоднозначный, но принимают второй.
Еще более странно, что первый пример работает, если ::v
заменяется на ::w
или наоборот. Не выведенные контексты c::
а также s< c >::
по-видимому, рассматриваются в порядке специализации, что не имеет смысла.
Я что-то упустил в стандарте, или все эти реализации имеют одну и ту же ошибку?
На мгновение переключаемся в крайне педантичный режим, да, я думаю, что вы что-то упускаете в стандарте, и нет, в этом случае это не должно иметь никакого значения.
Все стандартные ссылки на N4527, текущий рабочий проект.
[14.5.5.2p1] говорит:Для двух частичных специализаций шаблона класса первый Больше
специализированный чем второй, если, учитывая следующее переписать два
шаблоны функций, первый шаблон функции более специализирован
чем второй в соответствии с правилами упорядочения для шаблонов функций
(14.5.6.2):
- первый шаблон функции имеет те же параметры шаблона, что и первая частичная специализация, и имеет единственный параметр функции,
тип — это специализация шаблона класса с аргументами шаблона
первая частичная специализация, и- шаблон второй функции имеет те же параметры шаблона, что и вторая частичная специализация, и имеет один параметр функции
чей тип является специализацией шаблона класса с шаблоном
аргументы второй частичной специализации.
Переходя к [14.5.6.2p1]:
[…] Частичное оформление заказа перегруженных объявлений шаблонов функций
используется в следующих контекстах, чтобы выбрать шаблон функции для
к которой относится специализация шаблона функции:
- при разрешении перегрузки для вызова специализации шаблона функции (13.3.3);
- когда берется адрес специализации шаблона функции;
- когда выбирается оператор размещения, который является специализацией шаблона функции, чтобы соответствовать оператору размещения new (3.7.4.2,
5.3.4);- когда объявление функции друга (14.5.4), явная реализация (14.7.2) или явная специализация (14.7.3) ссылаются
к специализации шаблона функции.
Нет упоминания о частичном упорядочении специализаций шаблонов классов. Тем не менее, [14.8.2.4p3] говорит:
Типы, используемые для определения порядка, зависят от контекста в
который частичное упорядочение сделано:
- В контексте вызова функции используемые типы — это те типы параметров функции, для которых вызов функции имеет аргументы.
- В контексте вызова функции преобразования используются типы возврата шаблонов функции преобразования.
- В других контекстах (14.5.6.2) используется тип функции шаблона функции.
Даже если он ссылается на [14.5.6.2], он говорит «другие контексты». Я могу только заключить, что при применении алгоритма частичного упорядочения к шаблонам функций, сгенерированным в соответствии с правилами в [14.5.5.2], используется тип функции шаблона функции, а не список типов параметров, как это было бы для функции вызов.
Итак, выбор частичной специализации t
в вашем первом фрагменте будет эквивалентно не случаю, связанному с вызовом функции, а тому, который принимает адрес шаблона функции (например), который также попадает под «другие контексты»:
#include <iostream>
template<typename> struct s { typedef void v, w; };
template<typename, typename = void> struct t { };
template<typename C> void f(t<C, typename C::v>) { std::cout << "t<C, C::v>\n"; }
template<typename C> void f(t<s<C>, typename s<C>::w>) { std::cout << "t<s<C>, s<C>::w>\n"; }
int main()
{
using pft = void (*)(t<s<int>>);
pft p = f;
p(t<s<int>>());
}
(Поскольку мы все еще находимся в крайне педантичном режиме, я переписал шаблоны функций точно так же, как в примере в [14.5.5.2p2].)
Излишне говорить, что это также компилирует и печатает t<s<C>, s<C>::w>
, Шансы на то, что это приведет к другому поведению, были невелики, но мне пришлось это попробовать. Учитывая то, как работает алгоритм, он имел бы значение, если бы параметры функции были, например, ссылочными типами (вызывая специальные правила в [14.8.2.4] в случае вызова функции, но не в других случаях), но такие формы не могут возникать с шаблонами функций, сгенерированными из специализаций шаблонов классов.
Таким образом, весь этот обход не помог нам ни на минуту, но … это language-lawyer
вопрос, мы должны были иметь некоторые стандартные цитаты здесь …
Есть несколько активных основных проблем, связанных с вашим примером:
1157 содержит примечание, которое я считаю актуальным:
Вывод аргумента шаблона — это попытка сопоставить
P
и выведенный
A
; однако вычет аргумента шаблона не определен как сбой, если
P
и вывелA
несовместимы. Это может произойти в
наличие невыбранных контекстов. Несмотря на круглые скобки
заявление в 14.8.2.4 [temp.deduct.partial] параграф 9, шаблон
вычет аргумента может преуспеть в определении аргумента шаблона для
каждый параметр шаблона при создании выведенногоA
это не
совместим с соответствующимиP
,
Я не совсем уверен, что это так четко указано; в конце концов, [14.8.2.5p1] говорит
[…] найти значения аргументов шаблона […], которые сделают P после подстановки выведенных значений […] совместимым с A.
и [14.8.2.4] ссылается на [14.8.2.5] в полном объеме. Тем не менее, совершенно очевидно, что частичное упорядочение шаблонов функций не ищет совместимости, когда речь идет о не выведенных контекстах, и изменения, которые могут нарушить множество допустимых случаев, поэтому я думаю, что это просто отсутствие надлежащей спецификации в стандарте. ,
В меньшей степени, 1847 имеет отношение к непредсказуемым контекстам, появляющимся в аргументах специализаций шаблона. Это ссылки 1391 для разрешения; Я думаю, что есть некоторые проблемы с этой формулировкой — более подробно в этот ответ.
Для меня все это говорит о том, что ваш пример должен работать.
Как и вы, меня весьма заинтриговал тот факт, что в трех разных компиляторах присутствует одно и то же несоответствие. Я был еще более заинтригован после того, как убедился, что MSVC 14 демонстрирует точно такое же поведение, как и другие. Итак, когда у меня появилось время, я решил быстро взглянуть на то, что делает Clang; это оказалось совсем не быстро, но оно дало некоторые ответы.
Весь код, относящийся к нашему делу, находится в lib/Sema/SemaTemplateDeduction.cpp
.
Ядром алгоритма дедукции является DeduceTemplateArgumentsByTypeMatch
функция; все варианты дедукции в конечном итоге вызывают его, и затем он используется рекурсивно для обхода структуры составных типов, иногда с помощью сильно перегруженных DeduceTemplateArguments
набор функций, а некоторые флаги настроить алгоритм, основываясь на конкретном типе делающегося вывода и на частях формы, которые рассматриваются.
Важный аспект, который следует отметить относительно этой функции, заключается в том, что она обрабатывает строго дедукцию, а не замену. Он сравнивает формы типов, выводит значения аргументов шаблона для параметров шаблона, которые появляются в выведенных контекстах, и пропускает невыгруженные контексты. Единственная другая проверка, которую это делает, проверяет, что выведенные значения аргумента для параметра шаблона являются согласованными. Я написал еще немного о том, как Clang делает вывод во время частичного упорядочения в ответ, который я упомянул выше.
Для частичного упорядочения шаблонов функций алгоритм запускается в Sema::getMoreSpecializedTemplate
функция-член, которая использует флаг типа enum TPOC
определить контекст, для которого выполняется частичное упорядочение; счетчики TPOC_Call
, TPOC_Conversion
, а также TPOC_Other
; само за себя. Затем эта функция вызывает isAtLeastAsSpecializedAs
дважды, назад и вперед между двумя шаблонами, и сравнивает результаты.
isAtLeastAsSpecializedAs
включает значение TPOC
флаг, вносит некоторые коррективы, основанные на этом, и заканчивает тем, что звонит, прямо или косвенно, DeduceTemplateArgumentsByTypeMatch
. Если это вернется Sema::TDK_Success
, isAtLeastAsSpecializedAs
выполняет только еще одну проверку, чтобы убедиться, что все параметры шаблона, используемые для частичного упорядочения, имеют значения. Если это тоже хорошо, то возвращается true
,
И это частичный порядок для шаблонов функций. Основываясь на абзацах, приведенных в предыдущем разделе, я ожидал частичного упорядочения для вызова специализаций шаблона класса. Sema::getMoreSpecializedTemplate
с правильно сконструированными шаблонами функций и флагом TPOC_Other
и все будет течь естественно оттуда. Если бы это было так, ваш пример должен работать. Сюрприз: это не то, что происходит.
Частичное упорядочение для специализаций шаблона класса начинается в Sema::getMoreSpecializedPartialSpecialization
. В качестве оптимизации (красный флаг!) Он не синтезирует шаблоны функций, а использует DeduceTemplateArgumentsByTypeMatch
делать вывод типа непосредственно на самих шаблонах классов как на типах P
а также A
, Это отлично; в конце концов, это то, что алгоритм шаблонов функций в конечном итоге будет делать.
Однако, если все идет хорошо во время дедукции, он вызывает FinishTemplateArgumentDeduction
(перегрузка для специализаций шаблонов классов), которая выполняет подстановку и другие проверки, включая проверку того, что замещенные аргументы для специализации эквивалентны оригинальным. Это было бы хорошо, если бы код проверял, совпадает ли частичная специализация с набором аргументов, но не подходит во время частичного упорядочения, и, насколько я могу судить, вызывает проблему в вашем примере.
Итак, кажется, что Ричард Корден предположение относительно того, что происходит, верно, но я не совсем уверен, что это было сделано намеренно. Для меня это больше похоже на недосмотр. То, как мы в итоге вели себя так, что все компиляторы ведут себя одинаково, остается загадкой.
На мой взгляд, удаление двух звонков FinishTemplateArgumentDeduction
от Sema::getMoreSpecializedPartialSpecialization
не причинит вреда и восстановит согласованность с алгоритмом частичного упорядочения. Там нет необходимости для дополнительной проверки (сделано isAtLeastAsSpecializedAs
) что все параметры шаблона также имеют значения, поскольку мы знаем, что все параметры шаблона выводятся из аргументов специализации; если бы это было не так, частичная специализация потерпела бы неудачу в сопоставлении, поэтому мы бы в первую очередь не пошли на частичное упорядочение. (Допускаются ли такие частичные специализации в первую очередь, предметом выпуск 549. В таких случаях Clang выдает предупреждение, MSVC и GCC выдают ошибку. Во всяком случае, не проблема.)
Как примечание стороны, я думаю, что все это относится к перегрузка для переменных шаблонов специализаций также.
К сожалению, у меня нет настроенной среды сборки для Clang, поэтому я не могу проверить это изменение в данный момент.
Информация в этом ответе основана на большей части этот вопрос. Алгоритм частичного упорядочения шаблона не указан стандартом. Основные компиляторы, по крайней мере, согласны с тем, каким должен быть алгоритм.
Начнем с того, что ваши два примера не эквивалентны. У вас есть две специализации шаблона в дополнение к основному шаблону, но в примере с вашей функцией вы не добавляете перегрузку функции для основного. Если вы добавите это:
template <typename c>
constexpr int f( t<c> ) { return 0; }
Вызов функции также становится неоднозначным. Причина этого заключается в том, что алгоритм синтеза типов частичного упорядочения не создает экземпляры шаблонов, а вместо этого синтезирует новые уникальные типы.
Во-первых, если мы сравним функцию, которую я только что представил, с этой:
template< typename c >
constexpr int f( t< c, typename c::v > ) { return 1; }
У нас есть:
+---+---------------------+----------------------+
| | Parameters | Arguments |
+---+---------------------+----------------------+
| 0 | c, typename c::v | Unique0, void |
| 1 | c, void | Unique1, Unique1_v |
+---+---------------------+----------------------+
Мы игнорируем не выведенные контексты в правилах вывода частичного упорядочения, поэтому Unique0
Матчи c
, но Unique1_v
не совпадает void
! таким образом 0
является предпочтительным. Это, вероятно, не то, что вы ожидали.
Если мы тогда сравним 0
а также 2
:
+---+--------------------------+----------------------+
| | Parameters | Arguments |
+---+--------------------------+----------------------+
| 0 | s<c>, typename s<c>::w | Unique0, void |
| 2 | c, void | Unique2, Unique2_v |
+---+--------------------------+----------------------+
Здесь 0
вычет не удается (так как Unique0
не будет соответствовать s<c>
), но 2
вычет также не удается (так как Unique2_v
не будет соответствовать void
). Вот почему это неоднозначно.
Это привело меня к интересному вопросу о void_t
:
template <typename... >
using void_t = void;
Перегрузка этой функции:
template< typename c >
constexpr int f( t< s< c >, void_t<s<c>>> ) { return 3; }
будет предпочтительнее, чем 0
поскольку аргументы будут s<c>
а также void
, Но этот не будет
template <typename... >
struct make_void {
using type = void;
};
template< typename c >
constexpr int f( t< s< c >, typename make_void<s<c>>::type> ) { return 4; }
Поскольку мы не будем создавать экземпляр make_void<s<c>>
чтобы определить ::type
Таким образом, мы оказались в той же ситуации, что и 2
,
Я считаю, что цель состоит в том, чтобы примеры компилировались, однако в стандарте четко не указано, что должно происходить (если вообще что-либо) при сопоставлении списков аргументов шаблона для синтезированных списков аргументов, используемых частичным упорядочением (14.5.5.1/1):
Это делается путем сопоставления аргументов шаблона специализации шаблона класса со списками аргументов шаблона частичных специализаций.
Приведенный выше абзац обязателен, чтобы # 1 был выбран в следующем:
template <typename T, typename Q> struct A;
template <typename T> struct A<T, void> {}; #1
template <typename T> struct A<T, char> {}; #2
void foo ()
{
A<int, void> a;
}
Вот:
T
выводится int
(14.5.5.1/2)int
== int
, void
== void
(14.5.5.1/1)Для случая частичного заказа:
template< typename c > struct t< c, typename c::v > {}; #3
template< typename c > struct t< s< c >, typename s< c >::w > {}; #4
Для первого параметра # 4 является более специализированным, и оба вторых параметра являются не выведенными контекстами, т.е. вывод типа выполняется успешно от # 4 до # 3, но не для # 3 до # 4.
Я думаю, что компиляторы затем применяют правило «списки аргументов должны соответствовать» из 14.5.5.1/1 в списках синтезированных аргументов. Это сравнивает первый синтезированный тип Q1::v
ко второму s<Q2>::w
и эти типы не совпадают.
Это может объяснить, почему меняется v
в w
В результате некоторые примеры работали, так как компилятор решил, что эти типы одинаковы.
Это не проблема вне частичного упорядочения, потому что типы являются конкретными как типы, такие как c::v
будет создан для void
и т.п.
Возможно, что комитет намеревается применить эквивалентность типов (14.4), но я так не думаю. Стандарт, вероятно, должен просто прояснить, что именно должно происходить в отношении сопоставления (или нет) синтезированных типов, созданных как часть этапа частичного упорядочения.