Частичное упорядочение с шаблоном функции, имеющим неопределенный контекст
Читая другой вопрос, я столкнулся с проблемой частичного упорядочения, которую я сократил до следующего контрольного примера.
template<typename T>
struct Const { typedef void type; };
template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1
template<typename T>
void f(T, void*) { cout << "void*"; } // T2
int main() {
// GCC chokes on f(0, 0) (not being able to match against T1)
void *p = 0;
f(0, p);
}
Для обоих шаблонов функций тип функции специализации, которая вводит разрешение перегрузки, имеет вид void(int, void*)
, Но частичное упорядочение (согласно comeau и GCC) теперь говорит о том, что второй шаблон более специализирован. Но почему?
Позвольте мне пройти частичное упорядочение и показать, где у меня есть вопросы. май Q
быть уникальным составным типом, используемым для определения частичного заказа в соответствии с 14.5.5.2
,
- Преобразованный список параметров для
T1
(Q вставлено):(Q, typename Const<Q>::type*)
, Типы аргументов:AT
знак равно(Q, void*)
- Преобразованный список параметров для
T2
(Q вставлено):BT
знак равно(Q, void*)
, которые также являются типами аргументов. - Нетрансформированный список параметров для
T1
:(T, typename Const<T>::type*)
- Нетрансформированный список параметров для
T2
:(T, void*)
Так как C++03 недооценивает это, я использовал намерение, о котором читал в нескольких отчетах о дефектах. Вышеупомянутый преобразованный список параметров для T1
(называется AT
мной) используется в качестве списка аргументов для 14.8.2.1
Msgstr "Вывести аргументы шаблона из вызова функции".
14.8.2.1
не нужно преобразовывать AT
или же BT
сам по себе (например, удаление объявлений объявлений и т. д.), и идет прямо к 14.8.2.4
, который независимо для каждого A
/ P
пара делает вывод типа:
AT
противT2
:{
(Q, T)
,
(void*, void*)
}
,T
это единственный параметр шаблона здесь, и он найдет, чтоT
должно бытьQ
, Тип вычета успешно выполняется тривиально дляAT
противT2
,BT
противT1
:{
(Q, T)
,
(void*, typename Const<T>::type*)
}
, Найдет чтоT
являетсяQ
Тоже здесь.typename Const<T>::type*
это не выводимый контекст, и поэтому он не будет использоваться для вывода чего-либо.
Вот мой первый вопрос: будет ли это теперь использовать значение T
выводить по первому параметру? Если ответ "нет", то первый шаблон более специализирован. Этого не может быть, потому что и GCC, и Comeau говорят, что второй шаблон более специализирован, и я не верю, что они ошибаются. Итак, мы предполагаем "да" и вставляем void*
в T
, Абзац (14.8.2.4
) говорит: "Вычитание выполняется независимо для каждой пары, а затем результаты объединяются", а также "Однако в определенных контекстах значение не участвует в выводе типа, а вместо этого использует значения аргументов шаблона, которые были либо выведены в другом месте, либо явно указано ". Это звучит как "да" тоже.
Поэтому вычет также успешен для каждой пары A / P. Теперь каждый шаблон, по крайней мере, так же специализирован, как и другой, потому что дедукция также не основывается на каких-либо неявных преобразованиях и имеет успех в обоих направлениях. В результате звонок должен быть неоднозначным.
Итак, мой второй вопрос: теперь, почему реализации говорят, что второй шаблон более специализирован? Какой момент я упустил?
Изменить: я проверил явную специализацию и создание экземпляров, и оба, в последних версиях GCC (4.4
) скажите, что ссылка на специализацию неоднозначна, тогда как более старая версия GCC (4.1
) не возникает ошибка двусмысленности. Это говорит о том, что последние версии GCC имеют непоследовательное частичное упорядочение для шаблонов функций.
template<typename T>
struct Const { typedef void type; };
template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1
template<typename T>
void f(T, void*) { cout << "void*"; } // T2
template<> void f(int, void*) { }
// main.cpp:11: error: ambiguous template specialization
// 'f<>' for 'void f(int, void*)'
4 ответа
Вот мой путь в этом. Я согласен с Чарльзом Бэйли, что неверный шаг - это Const<Q>::Type*
в void*
template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1
template<typename T>
void f(T, void*) { cout << "void*"; } // T2
Шаги, которые мы хотим предпринять:
14.5.5.2/2
Учитывая два перегруженных шаблона функций, можно определить, является ли один более специализированным, чем другой, путем преобразования каждого шаблона по очереди и использования вывода аргумента (14.8.2) для сравнения его с другим.
14.5.5.2/3-b1
Для каждого параметра шаблона типа синтезируйте уникальный тип и заменяйте его для каждого вхождения этого параметра в списке параметров функции или для функции преобразования шаблона в возвращаемом типе.
На мой взгляд, типы синтезируются следующим образом:
(Q, Const<Q>::Type*) // Q1
(Q, void*) // Q2
Я не вижу никакой формулировки, которая требует, чтобы второй синтезированный параметр T1
быть void*
, Я не знаю ни одного прецедента для этого в других контекстах. Тип Const<Q>::Type*
является совершенно допустимым типом в системе типов C++.
Итак, теперь мы выполним шаги по удержанию:
Q2 до T1
Мы пытаемся вывести параметры шаблона для T1, поэтому имеем:
- Параметр 1:
T
выводитсяQ
- Параметр 2: не выводимый контекст
Несмотря на то, что параметр 2 - это не выводимый контекст, дедукция все же прошла успешно, потому что у нас есть значение для T.
Q1 до T2
Вывод параметров шаблона для T2 мы имеем:
- Параметр 1:
T
выводитсяQ
- Параметр 2:
void*
не совпадаетConst<Q>::Type*
так что вычет сбой.
ИМХО, вот где стандарт подводит нас. Параметр не зависит, поэтому не совсем понятно, что должно произойти, однако мой опыт (основанный на прищуренном чтении 14.8.2.1/3) заключается в том, что даже если тип параметра P не зависит, тип аргумента A должен совпадать Это.
Синтезированные аргументы T1 могут использоваться для специализации T2, но не наоборот. Поэтому T2 более специализирован, чем T1, и поэтому является лучшей функцией.
ОБНОВЛЕНИЕ 1:
Просто чтобы скрыть Const<Q>::type
быть пустым Рассмотрим следующий пример:
template<typename T>
struct Const;
template<typename T>
void f(T, typename Const<T>::type*) // T1
{ typedef typename T::TYPE1 TYPE; }
template<typename T>
void f(T, void*) // T2
{ typedef typename T::TYPE2 TYPE ; }
template<>
struct Const <int>
{
typedef void type;
};
template<>
struct Const <long>
{
typedef long type;
};
void bar ()
{
void * p = 0;
f (0, p);
}
В приведенном выше Const<int>::type
используется, когда мы выполняем обычные правила разрешения перегрузки, но не когда мы переходим к правилам частичной перегрузки. Было бы неправильно выбирать произвольную специализацию для Const<Q>::type
, Это может быть не интуитивно понятно, но компилятор очень рад иметь синтезированный тип формы Const<Q>::type*
и использовать его во время вывода типа.
ОБНОВЛЕНИЕ 2
template <typename T, int I>
class Const
{
public:
typedef typename Const<T, I-1>::type type;
};
template <typename T>
class Const <T, 0>
{
public:
typedef void type;
};
template<typename T, int I>
void f(T (&)[I], typename Const<T, I>::type*) // T1
{ typedef typename T::TYPE1 TYPE; }
template<typename T, int I>
void f(T (&)[I], void*) // T2
{ typedef typename T::TYPE2 TYPE ; }
void bar ()
{
int array[10];
void * p = 0;
f (array, p);
}
Когда Const
шаблон создается с некоторым значением I
рекурсивно реализует себя до I
достигает 0. Это когда частичная специализация Const<T,0>
выбран. Если у нас есть компилятор, который синтезирует некоторый реальный тип для параметров функции, то какое значение компилятор выберет для индекса массива? Скажи 10? Ну, это было бы хорошо для приведенного выше примера, но это не будет соответствовать частичной специализации Const<T, 10 + 1>
что, по крайней мере концептуально, приведет к бесконечному числу рекурсивных экземпляров первичного. Какое бы значение оно ни выбрало, мы могли бы изменить конечное условие таким образом, чтобы оно равнялось + 1, и тогда у нас был бы бесконечный цикл в алгоритме частичного упорядочения.
Я не вижу, как алгоритм частичного упорядочения мог правильно создать Const
найти что type
на самом деле.
Изменить: Изучив реализацию Clang (Дуг Грегор) их алгоритм частичного упорядочения, я пришел к согласию с остальными постерами, что оригинальный пример не "предназначен", чтобы быть неоднозначным - даже если стандарт не так ясен, как это может быть о том, что должно произойти в таких ситуациях. Я отредактировал этот пост, чтобы указать мои пересмотренные мысли (для моей собственной выгоды и ссылки). В частности, алгоритм Кланга пояснил, чтоtypename Const<T>::type
'не переводится в'void'на этапе частичного упорядочения - и что каждая пара A/P выводится независимо друг от друга.
Вначале я задавался вопросом, почему следующее было сочтено неоднозначным:
template<class T> void f(T,T*); // 1
template<class T> void f(T, int*); // 2
f(0, (int*)0); // ambiguous
(The above is ambiguous because one cannot deduce f1(U1,U1*) from f2(T,int*), and going the other way, one cannot deduce f2(U2,int*) from f1(T,T*). Neither is more specialized.)
но следующее не будет двусмысленным:
template<class T> struct X { typedef int type; };
template<class T> void f(T, typename X<T>::type*); // 3
template<class T> void f(T, int*); // 2
(Причина, по которой можно ожидать, что это будет неоднозначно, заключается в следующем:
- f3(U1,X<U1>::type*) -> f3(U1, int*) ==> f2(T,int*) (deduction ok, T=U1)
- f2(U2,int*) ==> f3(T, X<T>::type*) (deduction ok, T=U2 makes X<U2>::type* -> int*)
Если бы это было правдой, ни один из них не был бы более специализированным, чем другой.)
После изучения алгоритма частичного упорядочения Кланга становится ясно, что они трактуют "3" выше, как если бы это было:
template<class T, class S> void f(T, S*); // 4
так что вывод некоторого уникального 'U' против 'typename X::type' будет успешным -
f3(U1,X<U1>::type*) is treated as f3(U1, U2*) ==> f2(T,int*) (deduction not ok)
f2(U2,int*) ==> f3(T,S* [[X<T>::type*]]) (deduction ok, T=U2, S=int)
И поэтому "2" явно более специализирован, чем "3".
Редактировать: Пожалуйста, не обращайте внимания на этот пост - После изучения алгоритма частичного упорядочения clangs, реализованного Дагом Грегором (хотя он и был реализован лишь частично на момент написания этой статьи - кажется, что логика, которая имеет отношение к вопросу ОП, реализована достаточно адекватно), - он выглядит так, как будто он обрабатывает недоопределенный контекст как просто еще один параметр шаблона. Что говорит о том, что перегрузка с явным аргументом void* должна быть более специализированной версией и не должно быть никакой двусмысленности. Как обычно, Комо - это правильно. Теперь что касается формулировки в стандарте, которая четко определяет это поведение - это другое дело...
Так как этот пост был также опубликован на модерируемом comp.lang.C++. И, похоже, вызывает здесь некоторую путаницу - я подумал, что я тоже опубликую свой ответ в этой группе - поскольку обсуждение, очевидно, относится к заданному здесь вопросу,
On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:
You are going one step too fast here. How do you know (and would the compiler know) that there is no specialisation of Const<Q> such that Const<Q>::type != void?
As far as I can see, the compiler would transform the parameter-list of A to: AT=(Q, <unknown>*). To call B with these parameters requires an implicit conversion (<unknown>* to void*) and therefore A is less specialised than B.
Я считаю, что это неправильно. При проверке, чтобы увидеть, какая функция является более специализированной (во время частичного упорядочения), компилятор преобразует список параметров в (Q, void*)
- т.е. он фактически создает соответствующий шаблон (наилучшее соответствие) и ищет в нем значение 'type' - в этом случае, основываясь на основном шаблоне, он будет недействительным *.
Относительно вашей точки зрения относительно частичной специализации - при проверке того, какой шаблон является более специализированным, чем другой, единственный тип, который может использоваться, - это уникальный сгенерированный тип - если есть другие специализации в момент создания объявления (когда разрешение перегрузки является делается) они будут рассмотрены. Если вы добавите их позже, и они должны быть выбраны, вы будете нарушать ODR (в соответствии с 14.7.4.1)
Частичные / явные специализации также будут учитываться при формировании набора кандидатов, но на этот раз с использованием типов фактических аргументов функции. Если наилучшее соответствие частичной специализации (X) приводит к типу функции, которая имеет лучшую неявную последовательность преобразования для какого-либо параметра, то мы никогда не перейдем к фазе частичного упорядочения, и эта "лучшая" функция будет выбрана (до это к частичной фазе заказа)
Вот пример с комментариями о том, что должно происходить на разных этапах:
template<class T, bool=true> struct X; // Primary
template<class T> struct X<T,true> { typedef T type; }; // A
template<> struct X<int*,true> { typedef void* type; }; // B
template<class T> void f(T,typename X<T>::type); //1
template<class T> void f(T*,void*); //2
int main()
{
void* pv;
int* pi;
f(pi,pi);
// two candidate functions: f1<int*>(int*,void*), f2<int>(int*,void*)
// Note: specialization 'B' used to arrive at void* in f1
// neither has a better ICS than the other, so lets partially order
// transformed f1 is f1<U1>(U1,X<U1,true>::type) --> f1<U1>(U1,U1)
// (template 'A' used to get the second U1)
// obviously deduction will fail (U1,U1) -> (T*,void*)
// and also fails the other way (U2*, void*) -> (T,X<T>::type)
// can not partially order them - so ambiguity
f(pv,pv);
// two candidate functions: f1<void*>(void*,void*), f2<void>(void*,void*)
// Note: specialization 'A' used to arrive at second void* in f1
// neither has a better ICS than the other, so lets partially order
// transformed f1 is f1<U1>(U1,X<U1>::type) --> f1<U1>(U1,U1)
// (template 'A' used to get the second U1)
// obviously deduction will fail (U1,U1) -> (T*,void*)
// and also fails the other way (U2*, void*) -> (T,X<T>::type)
// can not partially order them - so ambiguity again
}
Стоит также упомянуть, что если основной шаблон не имеет определения, то SFINAE работает на этапе частичного упорядочения, и ни один из них не может быть выведен из другого, и это может привести к неоднозначности.
Также, если вы добавите другой шаблон, который приведет к другому совпадению, если точка создания какой-либо из этих функций будет перемещена в другое место в модуле перевода, вы явно нарушите ODR.
On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:
Во-первых, быть более специализированным означает, что это меньше типов, где этот шаблон может быть выбран с помощью разрешения перегрузки. Используя это, правила для частичного упорядочения могут быть обобщены следующим образом: Попытайтесь найти тип для A, такой, что A можно вызвать, но B нет, или разрешение перегрузки предпочитает вызывать A. Если этот тип может быть найден, то B более специализирован. чем А.
Здесь нет аргументов. Но, основываясь на правилах, какими они являются в настоящее время, пример ОП должен быть неоднозначным.
И, наконец, вот явные, однозначные ответы на два конкретных вопроса, поднятых litb:
1) Будет ли это теперь использовать значение T, выведенное для первого параметра?
Да, конечно, он должен делать вывод аргументов шаблона - необходимо поддерживать "ссылки".
2) Теперь, почему реализации говорят, что второе является более специализированным?
Потому что они не правы;)
Я надеюсь, что это решает проблему - пожалуйста, дайте мне знать, если что-то еще неясно:)
Edit: litb поднял хорошую мысль в своем комментарии - возможно, заявив, что первичный шаблон всегда будет использоваться для создания экземпляра с уникальным сгенерированным типом - слишком сильное утверждение.
Есть случаи, когда основной шаблон не будет вызван.
Я имею в виду, что при частичном упорядочении используется некий уникальный сгенерированный тип, соответствующий наилучшей специализации. Вы правы, это не обязательно должен быть основной шаблон. Я отредактировал вышеуказанный язык, чтобы сделать это. Он также поднял вопрос, касающийся определения лучшего подходящего шаблона после момента его создания. Это будет нарушением УСО в соответствии с разделом, посвященным конкретизации.
Стандарт гласит, что после создания пар A/P (с использованием правил преобразования, описанных в temp.func.order) они выводятся друг против друга с помощью вывода аргументов шаблона (temp.deduct)- и этот раздел обрабатывает случай невыгруженные контексты, создание экземпляра шаблона и его вложенного типа, запуск точек создания экземпляров. Секция temp.point обрабатывает нарушения ODR (значение частичного упорядочения не должно изменяться независимо от точек инстанции в единице перевода). Я до сих пор не уверен, откуда возникла путаница? - Фейсал Вали 1 час назад [удалить этот комментарий]
litb: "Обратите внимание, что шаг, который помещает Q в Const::type для построения аргументов, явно не охватывается правилом SFINAE. Правила SFINAE работают с вычетом аргументов, поместите абзацы, которые помещают Q в список параметров функции шаблона функции: в 14.5.5.2.'
Правила SFINAE должны быть использованы здесь - как они могут не быть? Я чувствую, что это подразумевается - я не буду отрицать, что это может быть более ясным, и хотя я призываю комитет уточнить это - я не думаю, что это нужно уточнить, чтобы достаточно толковать ваш пример.
Позвольте мне предоставить один способ связать их. Из (14.8.2): "Когда задан явный список аргументов шаблона, аргументы шаблона должны быть совместимы со списком параметров шаблона и приводить к допустимому типу функции, как описано ниже; в противном случае выведение типа завершится неудачно"
Из (14.5.5.2/3) "Используется следующее преобразование: - Для каждого параметра шаблона типа синтезируйте уникальный тип и заменяйте его для каждого вхождения этого параметра в списке параметров функции или для функции преобразования шаблона в возвращаемом тип."
На мой взгляд, приведенная выше цитата подразумевает, что после того, как вы "создадите" уникальные сгенерированные типы для каждого параметра шаблона, объявление функции должно быть реализовано в простоте путем явной подачи уникальных типов в качестве аргументов шаблона в наш шаблон функции. Если это приводит к неверному типу функции, то происходит сбой не только преобразования, но, что более важно, последующего вывода аргумента шаблона, необходимого для частичного упорядочения функции.
From (14.5.5.2/4) "Используя список параметров преобразованной функции, выполните вывод аргумента для другого шаблона функции. Преобразованный шаблон, по крайней мере, столь же специализирован, как и другой, если и только если, успешный вывод и типы выведенных параметров являются точным соответствием (поэтому вычет не зависит от неявных преобразований)."
Если преобразованный список параметров функции приводит к сбою замещения, то мы знаем, что удержание не могло быть успешным. И поскольку дедукция не удалась, она не так специализирована, как другие - это все, что нам нужно знать, чтобы приступить к частичному упорядочению этих двух.
litb: Я тоже не уверен, что происходит в этом случае:
template<typename T> struct A;
template<typename T> void f(T, typename A<T>::type); template<typename T> void f(T*, typename A<T>::type);
конечно, это с отступом, чтобы быть допустимым кодом, но при выполнении A::type он потерпит неудачу, потому что в контексте определения шаблона A еще не определен. "Также обратите внимание, что для экземпляров шаблона, полученных в результате такого подстановки, не определен POI при попытке определить порядок (частичный порядок не зависит ни от какого контекста. Это статическое свойство двух задействованных шаблонов функций). Я думаю, что это похоже на проблему в Стандарте, которую необходимо исправить.
Хорошо - я думаю, что я вижу, где мы видим вещи по-другому. Если я вас правильно понимаю, вы говорите, что по мере объявления этих шаблонов функций компилятор отслеживает частичное упорядочение среди них, независимо от разрешения перегрузки, когда-либо возникающего при выборе между ними. Если это то, как вы это интерпретируете, тогда я понимаю, почему вы ожидаете описанного выше поведения. Но я не думаю, что стандарт когда-либо требует или предписывает это.
Теперь в стандарте ясно, что частичное упорядочение не зависит от типа, используемого при вызове функции (я полагаю, что это то, на что вы ссылаетесь, когда вы описываете его как статическое свойство и оно не зависит от контекста).
Стандарт также ясно, что он заботится только о частичном упорядочении (вызывает частичное упорядочение) между шаблонами функций в процессе разрешения перегрузки (13.3.3/1) тогда и только тогда, когда он не может выбрать лучшую функцию на основе ICS или если это шаблон, а другой нет. [Частичное упорядочение частичных специализаций шаблона класса - отдельная проблема, и, на мой взгляд, использует соответствующий контекст (другие определения шаблона), который требует создания экземпляра этого конкретного класса.]
Таким образом, на мой взгляд, поскольку механизм частичного упорядочения шаблонов функций вызывается при выполнении разрешения перегрузки, он должен использовать соответствующую часть контекста (определения и специализации шаблонов), доступную в тот момент, когда выполняется разрешение перегрузки.,
Исходя из моей интерпретации, согласно вашему примеру, использующему "шаблонную структуру A" выше, код действителен. Частичное упорядочение не выполняется в контексте определения. Но если / когда вам случится вызвать разрешение перегрузки между двумя функциями, написав вызов f((int*)0,0) - и в то время, когда компилятор пытается собрать объявление кандидата или частично упорядочить их (если он переходит к шагу частичного упорядочения) если недопустимое выражение или тип является частью типа функции, SFINAE помогает нам и сообщает нам, что вывод шаблона завершается неудачно (что касается частичного упорядочения, то это означает, что нельзя больше специализируется, чем другие, если мы не могли даже преобразовать шаблон).
Теперь, что касается POI - если вы, как и я, убеждены, что преобразованные типы функций должны представлять неявные экземпляры, используя явно предоставленные списки аргументов шаблона (используя уникально сгенерированные типы), тогда уместны следующие стандартные кавычки:
14.6.4.1/1 Для специализации шаблона функции, специализации шаблона функции-члена или специализации для функции-члена или статического члена данных шаблона класса, если специализация создается неявно, так как на нее ссылаются из другой специализации шаблона и контекст, из которого он ссылается, зависит от параметра шаблона, точка создания специализации - точка создания включающей специализации.
Я интерпретирую это так, что POI преобразованного типа функции и типа функции origianl совпадает с POI для тех функций, которые созданы фактическим вызовом функции.
litb: Поскольку частичное упорядочение достаточно
a property of the syntactic form of parameters (i.e "T*" against "T(*)[N]"),
я бы проголосовал за внесение изменений в спецификацию (например, "если Q появляется во вложенном спецификаторе имени квалифицированного идентификатора, присваивающего имя типу, то именованным типом является"Q") Или сказать, что названный тип является другим уникальным типом.This means that in template<typename T> void f(T, typename Const<T>::type*);
the argument list is (Q, R*), for example.
Same for template<typename T> void f(T*, typename ConstI<sizeof(T)>::type);
the arg lisst would be (Q*, R). A similar rule would be needed for non-type parameters, of course.
Я должен был бы подумать об этом и сделать несколько тестовых случаев, чтобы увидеть, приведет ли это к естественным порядкам.
Ааа - теперь вы предлагаете возможное решение, которое решает двусмысленность в пользу того, что мы все интуитивно ожидаем, - это отдельная проблема, и хотя мне нравится направление, в котором вы движетесь, как и вы, мне тоже придется подумать в него, прежде чем объявить его работоспособность.
Спасибо за продолжение обсуждения. Я бы хотел, чтобы ТАК не ограничивал вас размещением комментариев.
Так как вы можете редактировать мои сообщения, пожалуйста, не стесняйтесь отвечать в сообщении, если это проще.
Преобразованный список параметров для T1 (вставлено Q): (Q, typename Const::type*). Типы аргументов: AT = (Q, void*)
Интересно, действительно ли это правильное упрощение? Когда вы синтезируете тип Q
Вам разрешено вызывать специализацию для Const
в целях определения порядка следования шаблонов?
template <>
struct Const<Q> { typedef int type; }
Это будет означать, что T2
по крайней мере, не так специализирован, как T1
потому что void*
параметр не совпадает T1
второй параметр для любого заданного параметра шаблона.