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