частичное упорядочение специализаций с не выводимым контекстом

Согласно [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 >:: по-видимому, рассматриваются в порядке специализации, что не имеет смысла.

Я что-то упустил в стандарте, или все эти реализации имеют одну и ту же ошибку?

8

Решение

На мгновение переключаемся в крайне педантичный режим, да, я думаю, что вы что-то упускаете в стандарте, и нет, в этом случае это не должно иметь никакого значения.

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

7

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

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


Начнем с того, что ваши два примера не эквивалентны. У вас есть две специализации шаблона в дополнение к основному шаблону, но в примере с вашей функцией вы не добавляете перегрузку функции для основного. Если вы добавите это:

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,

1

Я считаю, что цель состоит в том, чтобы примеры компилировались, однако в стандарте четко не указано, что должно происходить (если вообще что-либо) при сопоставлении списков аргументов шаблона для синтезированных списков аргументов, используемых частичным упорядочением (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;
}

Вот:

  1. Параметр шаблона T выводится int (14.5.5.1/2)
  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), но я так не думаю. Стандарт, вероятно, должен просто прояснить, что именно должно происходить в отношении сопоставления (или нет) синтезированных типов, созданных как часть этапа частичного упорядочения.

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