Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Universal references and std::initializer_list

Tags:

c++

c++11

In his "C++ and Beyond 2012: Universal References" presentation, Scott repeatedly stresses the point, that universal references handle/bind to everything and thus overloading a function that already takes a universal reference parameter does not make sense. I had no reason to doubt that until I mingled them with std::initializer_list.

Here is a short example:

#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;
}

Oddly enough, VC11 Nov 2012 CTP complains about ambiguity (error C2668: 'foo' : ambiguous call to overloaded function). Yet even more suprising is, that gcc-4.7.2, gcc-4.9.0 and clang-3.4 agree on the following output:

initializer list
initializer list
initializer list
universal reference

So apparently it is possible (with gcc and clang) to overload functions taking universal references with initializer_lists but when using the auto + { expr } => initializer_list-idiom it does even matter whether one takes the initializer_list by value or by const&. At least to me that behavior was totally surprising. Which behavior conforms to the standard? Does anyone know the logic behind that?

like image 755
user2523017 Avatar asked Jun 26 '13 08:06

user2523017


2 Answers

Here's the crux: Deducing a type from a braced-init-list ({expr...}) doesn't work for template arguments, only auto. With template arguments, you get a deduction failure, and the overload is removed from consideration. This leads to the first and third output.

it does even matter whether one takes the initializer_list by value or by const&

foo: For any X, two overloads taking X and X& parameters are ambiguous for an lvalue argument - both are equally viable (same for X vs X&& for rvalues).

struct X{};
void f(X);
void f(X&);
X x;
f(x); // error: ambiguous overloads

However, partial ordering rules step in here (§14.5.6.2), and the function taking a generic std::initializer_list is more specialized than the generic one taking anything.

goo: For two overloads with X& and X const& parameters and a X& argument, the first one is more viable because the second overload requires a Qualification conversion from X& to X const& (§13.3.3.1.2/1 Table 12 and §13.3.3.2/3 third sub-bullet).

like image 190
Xeo Avatar answered Oct 14 '22 06:10

Xeo


If Scott really says that he's wrong, and it's another problem with the misleading "universal references" mental model he's teaching.

So-called "universal references" are greedy, and might match when you don't want or expect them to, but that doesn't mean they are always the best match.

Non-template overloads can be an exact match and will be preferred to the "universal reference", e.g. this selects the non-template

bool f(int) { return true; }
template<typename T> void f(T&&) { }
bool b = f(0);

And template overloads can be more specialized than the "universal reference" and so will be chosen by overload resolution. e.g.

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 confirms that even f(T&) is more specialized than f(T&&) and will be preferred for lvalues.

In two of your cases the initializer_list overloads are not only more specialized, but a braced-init-list such as {1,2,3} can never be deduced by template argument deduction.

The explanation for your results is:

foo( {1,2,3} );

You cannot deduce a template argument from a braced-init-list, so deduction fails for foo(T&&) and foo(initializer_list<int>) is the only viable function.

foo( il );

foo(initializer_list<T>) is more specialized than foo(T&&) so is chosen by overload resolution.

goo( {1,2,3} );

You cannot deduce a template argument from a braced-init-list, so goo(initializer_list<int>) is the only viable function.

goo( il );

il is a non-const lvalue, goo(T&&) can be called with T deduced as initializer_list<int>&, so its signature is goo(initializer_list<int>&) which is a better match than goo(initializer_list<int> const&) because binding the non-const il to a const-reference is a worse conversion sequence than binding it to a non-const-reference.

One of the comments above quotes Scott's slides as saying: "Makes no sense: URefs handle everything." That's true, and that's exactly why you might want to overload! You might want a more specific function for certain types, and the universal reference function for everything else. You can also use SFINAE to constrain the universal reference function to stop it handling certain types, so that other overloads can handle them.

For an example in the standard library, std::async is an overloaded function taking universal references. One overload handles the case where the first argument is of type std::launch and the other overload handles everything else. SFINAE prevents the "everything else" overload from greedily matching calls that pass std::launch as the first argument.

like image 45
Jonathan Wakely Avatar answered Oct 14 '22 06:10

Jonathan Wakely