Подтвердить что ты не робот

Частичное упорядочение с шаблоном функции, имеющим неопределенный контекст

При чтении другого вопроса я столкнулся с проблемой частичного упорядочивания, которую я сократил до следующего тестового случая

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) теперь говорит о том, что второй шаблон более специализирован. Но почему?

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

Так как С++ 03 недоопределяет это, я использовал намерение, о котором я читал в нескольких отчетах о дефектах. Перечисленный выше список параметров для T1 (называемый мной AT) используется как список аргументов для 14.8.2.1 "Вывод аргументов шаблона из вызова функции".

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*)'
4b9b3361

Ответ 1

Вот иди сюда. Я согласен с Charles Bailey в том, что неправильный шаг - от 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* является вполне допустимым типом в системе типа С++.

Итак, теперь мы выполняем шаги вывода:

от 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:

Просто для того, чтобы покрыть poing о 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.

Ответ 2

Изменить: после изучения реализации 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".

Ответ 3

Преобразованный список параметров для T1 (Q вставлен): (Q, typename Const:: тип *). Типы аргументы: AT = (Q, void *)

Интересно, действительно ли это правильное упрощение. Когда вы синтезируете тип Q, разрешено ли вызывать специализацию для Const для определения порядка шаблонирования specliazation?

template <>
struct Const<Q> { typedef int type; }

Это означает, что T2 не является, по меньшей мере, таким же специализированным, как T1, потому что параметр void* не соответствует T1 второму параметру для любых заданных параметров шаблона.

Ответ 4

Изменить: Пожалуйста, не обращайте внимания на это сообщение. Изучив алгоритм clangs для частичного упорядочения, реализованный Дугом Грегором (хотя он частично реализован только с этой записью), кажется, что логика, относящаяся к вопросу OP, выполняется достаточно адекватно ) - кажется, что он рассматривает undeduced context как просто еще один параметр шаблона. Это говорит о том, что перегрузка с явным аргументом void * должна быть более специализированной версией, и не должно быть никакой двусмысленности. Как обычно, Комо прав. Теперь, что касается формулировки в стандарте, который четко определяет это поведение - это другое дело...

Так как этот пост был также размещен на comp.lang.С++. модерируется и, кажется, вызывает некоторую путаницу там тоже - я думал, что отправлю свой ответ на эту группу здесь тоже, поскольку обсуждение, очевидно, имеет отношение к здесь задан вопрос.

On Jul 25, 1:11 pm, Bart van Ingen Schenau <[email protected]> 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 <[email protected]> wrote:

Во-первых, быть более специализированным означает, что это меньше типов, этот шаблон может быть выбран с помощью разрешения перегрузки. Используя это, правила частичного упорядочения можно суммировать следующим образом: найти тип для A такой, что A можно назвать, но B нет или перегрузить разрешение предпочитает называть A. Если этот тип можно найти, тогда B больше специализированный, чем A.

Здесь нет аргументов. Но на основе правил, какими они являются в настоящее время, пример OP должен быть неоднозначным.


Наконец, здесь приведены явные, недвусмысленные ответы на два конкретных вопроса, поднятых litb:

1) Будет ли теперь использовать значение T, выведенное для первого параметра?
Да - конечно, он должен, он делает вывод аргумента шаблона - "ссылки" должны быть сохранены.

2) Теперь, почему в реализациях говорят, что второе является более специализированным?
Потому что они ошибаются;)

Я надеюсь, что это поставит проблему на отдых - Пожалуйста, дайте мне знать, если что-то еще неясно:)

Изменить: litb поднял хороший момент в своем комментарии - возможно, заявив, что первичный шаблон всегда будет используемый для создания экземпляра с уникальным сгенерированным типом, является слишком сильным утверждением. Есть случаи, когда первичный шаблон не будет вызываться.
То, что я получаю, заключается в том, что при частичном упорядочивании какой-то уникальный сгенерированный тип используемый для соответствия лучшей специализации. Вы правы, это не должен быть основной шаблон. Для этого я отредактировал этот язык. Он также поднял вопрос об определении лучшего шаблона соответствия после момента создания. Это будет нарушением ODR в соответствии с разделом о точке создания.


В стандарте указано, что после создания пар A/P (с использованием правил преобразования, описанных в temp.func.order) они выводятся друг против друга с использованием вычитания аргумента шаблона (temp.deduct) - и этот раздел обрабатывает случай не выводимых контекстов, создание экземпляра шаблона и его вложенного типа, запуск точек экземпляров. Раздел temp.point обрабатывает нарушения ODR (значение частичного упорядочения не должно меняться независимо от точек ввода внутри единицы перевода). Я все еще не уверен, откуда возникает путаница? - Faisal Vali 1 час назад [удалить этот комментарий]

litb: "Обратите внимание, что шаг, который ставит Q в Const:: type для построения аргументов, явно не рассматривается правилом SFINAE. Правила SFINAE работают с дедукцией аргумента, помещают абзацы, которые помещают Q в список параметров функции шаблона функции, в 14.5.5.2. '

Здесь должны использоваться правила SFINAE - как они могут быть? Я считаю, что это достаточно понятно - я не буду отрицать, что это может быть яснее, и хотя я призываю комитет разъяснить это - я не думаю, что нужно разъяснить, чтобы достаточно толковать ваш пример.

Позвольте мне предоставить один из способов связать их. Из (14.8.2): "Когда указан явный список аргументов шаблона, аргументы шаблона должны быть совместимы с список параметров шаблона и должен привести к действительному типу функции, как описано ниже; в противном случае вычесть тип не удается "

Из (14.5.5.2/3) "Используемое преобразование: - Для каждого параметра шаблона типа синтезируйте уникальный тип и замените его для каждого этот параметр в списке параметров функции или для функции преобразования шаблона в возвращаемом типе. "

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

Из (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:: он будет терпеть неудачу, поскольку на контекст определения шаблона, A еще не определен " Также обратите внимание, что POI, определенный для экземпляров шаблонов, полученных в результате этого вид подстановки при попытке определить порядок (частичный порядок не зависит от в любом контексте. Это статическое свойство двух шаблонов функций). Я думаю, что это похоже на проблему в стандарте, которая должна быть исправлена.

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

Теперь стандарт ясно, что частичное упорядочение агностически относится к типу, который используется при вызове функции (я считаю, это то, о чем вы говорите, когда описываете его как статическое свойство и оно не зависит от контекста).

Стандарт также ясно, что он заботится только о частичном заказе (вызывает частичный порядок заказов) между шаблонами функций во время процесса разрешения перегрузки (13.3.3/1) тогда и только тогда, когда он не мог выбрать лучшую функцию на основе ICS или если один шаблон, а другой нет. [Частичное упорядочение частичной специализации шаблона шаблона является отдельной проблемой и в моем сознании используется соответствующий контекст (другие определения шаблонов), который требует создания экземпляра этого конкретного класса.]

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

Итак, основываясь на моей интерпретации, в соответствии с вашим примером, используя "шаблон struct 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.Я должен был бы подумать об этом и сделать некоторые тестовые примеры, чтобы узнать, не приведет ли это к естественным упорядочениям.

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

Спасибо за продолжение обсуждения. Я бы хотел, чтобы SO не ограничивала вас комментариями.

Поскольку вы можете редактировать мои сообщения, пожалуйста, не стесняйтесь отвечать на сообщение, если это проще.