Универсальные ссылки и std:: initializer_list
В своей презентации "С++ и Beyond 2012: Universal References" Скотт неоднократно подчеркивает, что универсальные ссылки обрабатывают/связывают со всем и, таким образом, перегружают функцию, которая уже принимает универсальный ссылочный параметр, не имеет смысла.
У меня не было причин сомневаться в этом, пока я не смешал их с std::initializer_list
.
Вот краткий пример:
#include <iostream>
#include <initializer_list>
using namespace std;
template <typename T>
void foo(T&&) { cout << "universal reference" << endl; }
template <typename T>
void foo(initializer_list<T>) { cout << "initializer list" << endl; }
template <typename T>
void goo(T&&) { cout << "universal reference" << endl; }
template <typename T>
void goo(initializer_list<T> const&) { cout << "initializer list" << endl; }
int main(){
auto il = {4,5,6};
foo( {1,2,3} );
foo( il );
goo( {1,2,3} );
goo( il );
return 0;
}
Как ни странно, VC11 Nov 2012 CTP жалуется на двусмысленность (error C2668: 'foo' : ambiguous call to overloaded function
). Еще более удивительным является то, что gcc-4.7.2, gcc-4.9.0 и clang-3.4 соглашаются на следующий вывод:
initializer list
initializer list
initializer list
universal reference
По-видимому, возможно (с gcc и clang) перегружать функции, принимающие универсальные ссылки с помощью initializer_list
, но при использовании auto + { expr } => initializer_list
-idiom даже имеет значение, берет ли значение initializer_list
по значению или const&
.
По крайней мере, мне это поведение было совершенно неожиданным.
Какое поведение соответствует стандарту? Кто-нибудь знает логику этого?
Ответы
Ответ 1
Здесь crux: выведение типа из списка braced-init-list ({expr...}
) не работает для аргументов шаблона, только auto
. С аргументами шаблона вы получаете отказ от вычета, и перегрузка удаляется из рассмотрения. Это приводит к первому и третьему выходам.
даже важно, берет ли значение initializer_list
по значению или const&
foo
: для любого X
две перегрузки с параметрами X
и X&
неоднозначны для аргумента lvalue - обе одинаково жизнеспособны (одинаковы для X
vs X&&
для rvalues).
struct X{};
void f(X);
void f(X&);
X x;
f(x); // error: ambiguous overloads
Однако здесь применяются правила частичного упорядочения (§14.5.6.2), а функция, принимающая общий std::initializer_list
, более специализирована, чем общий, который принимает что-либо.
goo
: для двух перегрузок с параметрами X&
и X const&
и аргументом X&
первый из них более жизнеспособен, потому что для второй перегрузки требуется преобразование Квалификации от X&
до X const&
(§ 13.3.3.1.2/1 Таблица 12 и §13.3.3.2/3 третья подпункта).
Ответ 2
Если Скотт действительно говорит, что он ошибается, и это еще одна проблема с вводящей в заблуждение моделью "универсальных ссылок", которую он преподаёт.
Так называемые "универсальные ссылки" являются жадными и могут совпадать, когда вы этого не хотите или ожидаете, но это не значит, что они всегда наилучшим образом соответствуют.
Не-шаблонные перегрузки могут быть точным совпадением и будут предпочтительнее "универсальной ссылки", например. это выбирает не-шаблон
bool f(int) { return true; }
template<typename T> void f(T&&) { }
bool b = f(0);
И перегрузки шаблонов могут быть более специализированными, чем "универсальная ссылка" и поэтому будут выбраны с помощью разрешения перегрузки. например.
template<typename T> struct A { };
template<typename T> void f(T&&) { }
template<typename T> bool f(A<T>) { return true; }
bool b = f(A<int>());
DR 1164 подтверждает, что даже f(T&)
более специализирован, чем f(T&&)
, и будет предпочтительнее для lvalues.
В двух ваших случаях перегрузки initializer_list
не только более специализированы, но и список с расширенным набором команд, например {1,2,3}
, никогда не могут быть выведены путем вычитания аргумента шаблона.
Объяснение ваших результатов:
foo( {1,2,3} );
Вы не можете вывести аргумент шаблона из списка braced-init-list, поэтому вывод не выполняется для foo(T&&)
и foo(initializer_list<int>)
является единственной жизнеспособной функцией.
foo( il );
foo(initializer_list<T>)
более специализирован, чем foo(T&&)
, поэтому выбирается с помощью разрешения перегрузки.
goo( {1,2,3} );
Вы не можете вывести аргумент шаблона из скопированного списка init, поэтому goo(initializer_list<int>)
- единственная жизнеспособная функция.
goo( il );
il
является неконстантным lvalue, goo(T&&)
можно вызывать с T
, выведенным как initializer_list<int>&
, поэтому его подпись goo(initializer_list<int>&)
, которая лучше, чем goo(initializer_list<int> const&)
, потому что привязка не- const il
к const-ссылке является худшей последовательностью преобразования, чем привязка ее к не-const-ссылке.
Один из комментариев выше приводит цитаты Скотта: "Не имеет смысла: URefs обрабатывают все". Это правда, и именно поэтому вы можете перегрузить! Вам может понадобиться более конкретная функция для определенных типов и универсальная эталонная функция для всего остального. Вы также можете использовать SFINAE, чтобы ограничить универсальную опорную функцию, чтобы остановить ее обработку определенных типов, чтобы другие перегрузки могли обрабатывать их.
Для примера в стандартной библиотеке std::async
- это перегруженная функция, использующая универсальные ссылки. Одна перегрузка обрабатывает случай, когда первый аргумент имеет тип std::launch
, а другая перегрузка обрабатывает все остальное. SFINAE предотвращает перегрузку "все остальное" из жадно соответствующих вызовов, которые передают std::launch
в качестве первого аргумента.
Ответ 3
Итак, сначала реакция на foo
имеет смысл. initializer_list<T>
соответствует обеим вызовам и является более специализированным, поэтому его следует называть таким образом.
Для goo
это синхронизируется с идеальной пересылкой. при вызове goo(il)
существует выбор между goo(T&&)
(с T = initializer_list<T>&
) и константной ссылочной версией. Я предполагаю, что вызов версии с неконстантной ссылкой имеет приоритет над более специализированной версией с ссылкой на const. При этом я не уверен, что это четко определенная ситуация w.r.t. стандарт.
Edit:
Обратите внимание, что если шаблона не было, это будет разрешено в пункте 13.3.3.2 (ранжирование неявных последовательностей преобразования) стандарта. Проблема здесь в том, что AFAIK, частичный порядок шаблона, определял бы второй (более специализированный) goo(initializer_list<T> const&)
, который должен быть вызван, но ранжирование на неявных конверсионных последовательностях будет требовать вызова goo(T&&)
. Поэтому я предполагаю, что это двусмысленный случай.