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

Частичное упорядочение шаблонов функций - неоднозначный вызов

Рассмотрим этот фрагмент кода С++ 11:

#include <iostream>
#include <cstddef>

template<typename T> void f(T, const char*) //#1
{ 
    std::cout << "f(T, const char*)\n"; 
}

template<std::size_t N> void f(int, const char(&)[N]) //#2
{ 
    std::cout << "f(int, const char (&)[N])\n"; 
}

int main()
{
    f(7, "ab");
}

Хорошо, так... какая перегрузка выбрана? Прежде чем выпустить beans с выходом компилятора, попробуйте объяснить это.

(Все ссылки на разделы предназначены для окончательного стандартного документа для С++ 11, ISO/IEC 14882: 2011.)

T из # 1 выводится на int, N из # 2 выводится на 3, обе специализации являются кандидатами, обе являются жизнеспособными, настолько хорошими. Какой из них лучше?

Во-первых, рассматриваются неявные преобразования, необходимые для сопоставления аргументов функции с параметрами функции. Для первого аргумента преобразование не требуется в любом случае (преобразование идентичности), int всюду, поэтому обе функции одинаково хороши. Для второго типа аргумента const char[3], а два преобразования:

  • для # 1, преобразование матрицы в указатель, преобразование категории lvalue, согласно [13.3.3.1.1]; эта категория преобразования игнорируется при сравнении последовательностей преобразования в соответствии с [13.3.3.2], так что это в основном то же самое, что и преобразование идентичности для этой цели;
  • для # 2, параметр имеет ссылочный тип и привязывается непосредственно к аргументу, поэтому, согласно [13.3.3.1.4], это снова преобразование идентичности.

Опять же, не повезло: эти две функции по-прежнему одинаково хороши. Оба являются шаблонами специализации, теперь мы должны увидеть, какой шаблон функции, если таковой имеется, более специализирован ([14.5.6.2] и [14.8.2.4]).

ИЗМЕНИТЬ 3: Нижеприведенное описание близко, но не совсем точно. См. Мой ответ за то, что я считаю правильным описанием процесса.

  • Вывод аргумента шаблона с №1 в качестве параметра и # 2 в качестве аргумента: мы выставляем значение M для замены N, T, выведенного как int, const char*, поскольку параметр может быть инициализирован из аргумент типа char[M], все отлично. Насколько я могу судить, # 2 по крайней мере так же специализирован, как # 1 для всех задействованных типов.
  • Вывод аргумента шаблона С# 2 как параметр и # 1 в качестве аргумента: мы изобретаем тип U для замены T, параметр типа int не может быть инициализирован из аргумента типа U ( несвязанные типы) параметр типа char[N] не может быть инициализирован из аргумента типа const char*, а значение параметра non-type N не может быть выведено из аргументов, поэтому... все не удается. Насколько я могу судить, # 1 не является, по крайней мере, таким же специализированным, как # 2 для всех задействованных типов.

РЕДАКТИРОВАТЬ 1: Вышеизложенное было отредактировано на основе комментариев от Columbo и dyp, чтобы отразить тот факт, что ссылки удаляются перед попыткой вывода аргумента шаблона в этом случае.

РЕДАКТИРОВАТЬ 2: На основе информации с hvd также удаляются cv-квалификаторы верхнего уровня. В этом случае это означает, что const char[N] становится char[N], потому что cv-определители на элементах массива также применяются к самому массиву (так как array of const также является const array); это вообще не было очевидным в стандарте С++ 11, но было уточнено для С++ 14.

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

Теперь вернемся к суровой реальности. Как GCC 4.9.1, так и Clang 3.5.0 со следующими параметрами

-Wall -Wextra -std=c++11 -pedantic 

отклонить вызов как неоднозначный, с похожими сообщениями об ошибках. Ошибка от Clang:

prog.cc:16:2: error: call to 'f' is ambiguous
    f(7, "ab");
    ^
prog.cc:4:27: note: candidate function [with T = int] 
template<typename T> void f(T, const char*) //#1 
                         ^
prog.cc:9:30: note: candidate function [with N = 3] 
template<std::size_t N> void f(int, const char(&)[N]) //#2 
                            ^

Visual С++ 2013 IntelliSense (на основе компилятора EDG, насколько я знаю) также помещает вызов как неоднозначный. Как ни странно, компилятор VС++ идет вперед и компилирует код без ошибок, выбирая # 2. (Я согласен со мной, поэтому он должен быть прав.)

Очевидный вопрос для экспертов: почему вызов неоднозначен? Что мне не хватает (в области частичного заказа, я бы догадался)?

4b9b3361

Ответ 1

Я рассказываю подробности моего нынешнего понимания проблемы в качестве ответа. Я не уверен, что это будет последнее слово в этом вопросе, но это может послужить основой для дальнейшего обсуждения, если это необходимо. Замечания от dyp, hvd и Columbo были необходимы для поиска различных бит информации, упомянутых ниже.

Как я подозревал, проблема связана с правилами частичного упорядочения шаблонов функций. Раздел [14.8.2.4] (Вывод аргументов шаблона при частичном упорядочении) говорит, что после предварительных преобразований, которые удаляют ссылки и cv-квалификаторы, вывод типа выполняется так, как описано в [14.8.2.5] (вывод из аргументов шаблона из типа). Этот раздел отличается от того, который ссылается на вызовы функций - это будет [14.8.2.1] (вычитание аргументов шаблона из вызова функции).

Когда параметры шаблона выводятся из типов аргументов функции, допускается несколько особых случаев; например, параметр шаблона T, используемый в функциональном параметре типа T*, может быть выведен, когда аргумент функции T[i], потому что преобразование от массива к указателю допускается в этом случае. Однако это не процесс вывода, который использовался при частичном упорядочении, хотя мы все еще говорим о функциях.

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

Очистить как грязь? Возможно, несколько примеров помогут.

Это работает, потому что он использует правила для вывода аргументов шаблона из вызова функции:

#include <iostream>
#include <type_traits>

template<typename T> void f(T*)
{
    std::cout << std::is_same<T, int>::value << '\n';
}

int main()
{
    int a[3];
    f(a);
}

и печатает 1.

Это не так, потому что он использует правила для вывода аргументов шаблона из типа:

#include <iostream>

template<typename T> struct A;

template<typename T> struct A<T*>
{
    static void f() { std::cout << "specialization\n"; }
};

int main()
{
    A<int[3]>::f();
}

и ошибка от Clang равна

error: implicit instantiation of undefined template 'A<int [3]>'

Специализация не может использоваться, потому что T* и int[3] не совпадают в этом случае, поэтому компилятор пытается создать экземпляр первичного шаблона.

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


Вернемся к объявлениям шаблонов функций:

template<typename T> void f(T, const char*); //#1
template<std::size_t N> void f(int, const char(&)[N]); //#2

Мое описание процесса частичного упорядочения будет:

  • Вывод аргумента шаблона С# 1 в качестве параметра и # 2 в качестве аргумента: мы выставляем значение M для замены N, T выводится как int, но параметр типа const char* не соответствует аргументу типа char[M], поэтому # 2 - не, по крайней мере, как специализированный, как # 1 для второй пары типов.
  • Вывод аргумента шаблона С# 2 как параметр и # 1 в качестве аргумента: мы изобретаем тип U для замены T, int и U не совпадают (разные типы), параметр типа char[N] не соответствует аргументу типа const char*, а значение параметра шаблона не-типа N не может быть выведено из аргументов, поэтому # 1 не, по крайней мере, как специализированный как # 2 для любой пары типов.

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


Объяснение выше несколько противоречит описанию аналогичной проблемы в Core Language Active Issue 1610 (ссылка предоставлена ​​hvd).

Пример:

template<class C> void foo(const C* val) {}
template<int N> void foo(const char (&t)[N]) {}

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

Затем он объясняет, что причиной является удаление квалификатора const от const char[N], что дает char[N], что приводит к отказу вывода с параметром const C*.

Однако, исходя из моего нынешнего понимания, вычет в этом случае не завершится, const или no const. Это подтверждается текущими реализациями в Clang и GCC: если мы удалим квалификатор const из параметров обоих шаблонов функций и вызов foo() с аргументом char[3], вызов по-прежнему неоднозначен. Массивы и указатели просто не соответствуют текущим правилам во время частичного упорядочивания.

Сказав это, я не являюсь членом комитета, поэтому может быть и больше, чем я понимаю.


Обновление. Недавно я наткнулся на другую активную проблему Core, которая восходит к 2003 году: issue 402.

Пример в нем эквивалентен тому, который находится в 1610. Замечания по проблеме дают понять, что две перегрузки неупорядочены в соответствии с алгоритмом частичного упорядочения в том виде, в котором она существует, именно из-за отсутствия правил распада матриц-указателей при частичном упорядочении.

Последний комментарий:

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

Таким образом, я уверен, что интерпретация, которую я дал выше, верна.

Ответ 2

Изначально я думал, что проблема с вашим кодом заключается в том, что вы не учитывали настройку типа функции. Регулировка типа функции заставляет массив с границами интерпретироваться как указатель на тип. Я попытался найти решение вашей проблемы, попросив компилятор, что он статически статирует с помощью шаблонов, но вместо этого получаю более интересные результаты:

#include <iostream>
#include <type_traits>

template<typename T, std::size_t N>
void is_same( const T* _left, const char(&_right)[N] )
{
 typedef decltype(_left) LeftT;
 typedef decltype(_right) RightT;

 std::cout << std::is_same<LeftT,const char*>::value << std::endl;
 std::cout << std::is_same<LeftT,const char(&)[3]>::value << std::endl;
 std::cout << std::is_same<LeftT,const char(&)[4]>::value << std::endl;
 std::cout << std::is_same<RightT,const char*>::value << std::endl;
 std::cout << std::is_same<RightT,const char(&)[3]>::value << std::endl;
 std::cout << std::is_same<RightT,const char(&)[4]>::value << std::endl;
}

int main()
{
 std::cout << std::boolalpha;

 is_same( "ab", "cd" );

 return 0;
}

Выход дает: правда ложный ложный ложный правда ложь

Компилятор способен различать аргументы в этом случае.

Изменить 1: Вот еще код. Введение ссылок rvalue делает функции более различимыми.

#include <iostream>

// f
template<typename _T>
 void f( _T, const char* )
 {
  std::cout << "f( _T, const char* )" << std::endl;
 }

template<std::size_t _kN>
 void f( int, const char(&)[_kN] )
 {
  std::cout << "f( int, const char (&)[_kN] )" << std::endl;
 }

// g
template<typename _T>
 void g( _T, const char* )
 {
  std::cout << "g( _T, const char* )" << std::endl;
 }

template<std::size_t _kN>
 void g( int, const char(&&)[_kN] )
 {
  std::cout << "g( int, const char (&&)[_kN] )" << std::endl;
 }

// h
template<std::size_t _kN>
 void h( int, const char(&)[_kN] )
 {
  std::cout << "h( int, const char(&)[_kN] )" << std::endl;
 }

template<std::size_t _kN>
 void h( int, const char(&&)[_kN] )
 {
  std::cout << "h( int, const char (&&)[_kN] )" << std::endl;
 }

int main()
{
 //f( 7, "ab" ); // Error!
 //f( 7, std::move("ab") ); // Error!
 f( 7, static_cast<const char*>("ab") ); // OK
 //f( 7, static_cast<const char(&)[3]>("ab") ); // Error!
 //f( 7, static_cast<const char(&&)[3]>("ab") ); // Error!

 g( 7, "ab" ); // OK
 //g( 7, std::move("ab") ); // Error!
 g( 7, static_cast<const char*>("ab") ); // OK
 g( 7, static_cast<const char(&)[3]>("ab") ); // OK
 //g( 7, static_cast<const char (&&)[3]>("ab") ); // Error!

 h( 7, "ab" ); // OK (What? Why is this an lvalue?)
 h( 7, std::move("ab") ); // OK
 //h( 7, static_cast<const char*>("ab") ); // Error
 h( 7, static_cast<const char(&)[3]>("ab") ); // OK
 h( 7, static_cast<const char(&&)[3]>("ab") ); // OK

 return 0;
}