14

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?

user2523017
  • 143
  • 4
  • The last one is an example of [over.ics.rank]/3 sub-bullet 6, stating that the overload whose parameter type is less cv-qualified will be used unambiguously. – dyp Jun 26 '13 at 08:28
  • 2
    Are you sure there was a statement in the presentation as general as you claim? It's true that a universal reference will bind to anything, but it's obviously not true that the overload disambiguation mechanism (for templates, that is) will always rank the universal-reference one as the most specific overload. – jogojapan Jun 26 '13 at 08:30
  • Skipping briefly through the slides, on slide 17 it says: `Overloading + URef almost always an error. Makes no sense: URefs handle everything. [...]` – user2523017 Jun 26 '13 at 08:59
  • 1
    @user2523017 Ok. The rules used to rank template overloads are not easy to understand. I could be wrong (although Xeo's answer seems to agree), but I'd have assumed naturally that `template void f(initializer_list)` is more specialized than `template void f(T&&)`. Simple tests with GCC seem to confirm this too (not necessarily using `initializer_list` as container). – jogojapan Jun 26 '13 at 09:10
  • @jogojapan It is true that `template void f(initializer_list)` is more specialized than `template void f(T&&)` but the crux is *it doesn't care*. Overload selection *first* ranks based on the conversion sequence, then **if there's no best overload yet** on some other criteria including partial ordering ("more specialized"). – dyp Jun 26 '13 at 10:01
  • Check out my article on this topic here: http://mortoray.com/2013/06/03/overriding-the-broken-universal-reference-t/ – edA-qa mort-ora-y Jun 26 '13 at 11:11
  • @DyP I might misunderstand parts of what you are saying, but I have a suspicion that you confuse ordinary function overload resolution (§13.3) with function template overload resolution (§14.5.6.2). Both processes can be combined if a function template competes with a non-template function, but in the cases discussed here, it's just two templates competing. I don't see how the conversion-sequence based ranking (which is for function overload, not template overload) applies there. – jogojapan Jun 26 '13 at 13:21

3 Answers3

9

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).

Xeo
  • 129,499
  • 52
  • 291
  • 397
  • Sadly, the standard doesn't seem to specify what happens if the specialization criterion and the more viable criterion contradict each other on which function is to be used ... – PierreBdR Jun 26 '13 at 08:47
  • 1
    @PierreBdR: It does, the cases for determining the *best viable function* are looked at in the order they appear in. – Xeo Jun 26 '13 at 08:51
4

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.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
0

Ok, so first the reaction to foo makes sense. initializer_list<T> match both calls and is more specialized, therefore should be called this way.

For goo, this is in sync with perfect forwarding. when calling goo(il), there is the choice between goo(T&&) (with T = initializer_list<T>&) and the constant reference version. I guess calling the version with the non-const reference has precedence over the more specialized version with the const reference. That being said, I am not sure this is a well defined situation w.r.t. the standard.

Edit:

Note that if there were no template, that would be resolved by the paragraph 13.3.3.2 (Ranking implicit conversion sequences) of the standard. The problem here is that, AFAIK, the partial ordering of template function would dictate the second (more specialized) goo(initializer_list<T> const&) is to be called, but the ranking on implicit conversion sequences would dictate that goo(T&&) is to be called. So I guess this is an ambiguous case.

PierreBdR
  • 42,120
  • 10
  • 46
  • 62
  • @DyP: There are no non-template functions here. – Xeo Jun 26 '13 at 08:34
  • @Xeo Oh, I'm too tired... yeah – dyp Jun 26 '13 at 08:34
  • @DyP I know, but templates complicate things. I added a note to explain that. – PierreBdR Jun 26 '13 at 08:45
  • 1
    @PierreBdR: Implicit-conversion-sequences are handled before partial-ordering, making the first overload more viable. – Xeo Jun 26 '13 at 08:46
  • @Xeo I guess you nailed it. Both conversion sequences are of rank "Exact Match", but [over.ics.rank]/3 explicitly prefers the conversion w/o qualification adjustment. [over.match.best]/1 states as you said that implicit conversion sequences are used before partial ordering to determine the best overload. – dyp Jun 26 '13 at 08:53
  • @DyP: Yep, the wording "or, if not that," at the end of every bullet should make it clear that they're looked at in order. – Xeo Jun 26 '13 at 08:58
  • 1
    @Xeo I'd go with "Given these definitions, a viable function *F1* is defined to be a better function than another viable function *F2* ***if*** for all arguments i, ICS*i*(F1) **is not a worse conversion sequence than ICSi(F2), and then**" ;) – dyp Jun 26 '13 at 09:10