Частичное упорядочение шаблонов функций - неоднозначный вызов
Рассмотрим этот фрагмент кода С++ 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. (Я согласен со мной, поэтому он должен быть прав.)
Очевидный вопрос для экспертов: почему вызов неоднозначен? Что мне не хватает (в области частичного заказа, я бы догадался)?
Ответы
Ответ 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;
}