Частичное упорядочение специализаций с не выводимым контекстом
Согласно [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 >::
по-видимому, рассматриваются в порядке специализации, что не имеет смысла.
Я что-то упустил в стандарте, или все эти реализации имеют одну и ту же ошибку?
3 ответа
На мгновение переключаясь в режим экстремально педантичный, да, я думаю, что вы что-то упускаете в стандарте, и нет, в этом случае это не должно иметь никакого значения.
Все стандартные ссылки на 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
несовместимы. Это может происходить при наличии не выводимых контекстов. Несмотря на заключенное в скобки утверждение в параграфе 9 раздела 14.8.2.4 [temp.deduct.partial], вычет аргумента шаблона может успешно определить аргумент шаблона для каждого параметра шаблона при создании выведенного значения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
набор функций и некоторые флаги для настройки алгоритма на основе конкретного типа выполняемого вывода и рассматриваемых частей формы типа.
Важный аспект, который следует отметить относительно этой функции, заключается в том, что она обрабатывает строго дедукцию, а не замену. Он сравнивает формы типов, выводит значения аргументов шаблона для параметров шаблона, которые появляются в выведенных контекстах, и пропускает невыгруженные контексты. Единственная другая проверка, которую он выполняет, - это проверка того, что выведенные значения аргумента для параметра шаблона являются согласованными. В ответе, о котором я упоминал выше, я написал больше о том, как Кланг делает дедукцию при частичном упорядочении.
Для частичного упорядочения шаблонов функций алгоритм запускается в 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
(перегрузка для специализаций шаблонов классов), которая выполняет подстановку и другие проверки, включая проверку того, что замещенные аргументы для специализации эквивалентны исходным. Это было бы хорошо, если бы код проверял, совпадает ли частичная специализация с набором аргументов, но не подходит во время частичного упорядочения, и, насколько я могу судить, вызывает проблему в вашем примере.
Таким образом, кажется, что предположение Richard Corden того, что происходит, является правильным, но я не совсем уверен, что это было сделано намеренно. Для меня это больше похоже на недосмотр. То, как мы в итоге вели себя так, что все компиляторы ведут себя одинаково, остается загадкой.
На мой взгляд, удаление двух звонков FinishTemplateArgumentDeduction
от Sema::getMoreSpecializedPartialSpecialization
не причинит вреда и восстановит согласованность с алгоритмом частичного упорядочения. Там нет необходимости для дополнительной проверки (сделано isAtLeastAsSpecializedAs
) что все параметры шаблона также имеют значения, поскольку мы знаем, что все параметры шаблона выводятся из аргументов специализации; если бы это было не так, частичная специализация потерпела бы неудачу в сопоставлении, поэтому в первую очередь мы не получили бы частичное упорядочение. (Разрешены ли такие частичные специализации в первую очередь, это тема вопроса 549. Clang выдает предупреждение для таких случаев, MSVC и GCC выдают ошибку. В любом случае, это не проблема.)
В качестве примечания, я думаю, что все это относится и к перегрузке для специализаций с переменными шаблонами.
К сожалению, у меня нет настроенной среды сборки для Clang, поэтому я не могу проверить это изменение в данный момент.
Я считаю, что цель состоит в том, чтобы примеры компилировались, однако в стандарте четко не указано, что должно произойти (если вообще что-то) при сопоставлении списков аргументов шаблона для синтезированных списков аргументов, используемых частичным упорядочением (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), но я так не думаю. Стандарт, вероятно, должен просто прояснить, что именно должно происходить в отношении сопоставления (или нет) синтезированных типов, созданных как часть этапа частичного упорядочения.
Информация в этом ответе основана на большей части этого вопроса. Алгоритм частичного упорядочения шаблона не указан стандартом. Основные компиляторы, по крайней мере, согласны с тем, каким должен быть алгоритм.
Начнем с того, что ваши два примера не эквивалентны. У вас есть две специализации шаблона в дополнение к первичному шаблону, но в вашем примере функции вы не добавляете перегрузку функции для первичного. Если вы добавите это:
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
,