Universal references and std::initializer_list
Asked Answered
H

3

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?

Herring answered 26/6, 2013 at 8:3 Comment(7)
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.Demonize
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.Intravenous
Skipping briefly through the slides, on slide 17 it says: Overloading + URef almost always an error. Makes no sense: URefs handle everything. [...]Herring
@Herring 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 <class T> void f(initializer_list<T>) is more specialized than template <class T> void f(T&&). Simple tests with GCC seem to confirm this too (not necessarily using initializer_list as container).Intravenous
@Intravenous It is true that template <class T> void f(initializer_list<T>) is more specialized than template <class T> 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").Demonize
Check out my article on this topic here: mortoray.com/2013/06/03/…Nickeliferous
@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.Intravenous
A
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).

Arezzo answered 26/6, 2013 at 8:19 Comment(2)
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 ...Diffuse
@PierreBdR: It does, the cases for determining the best viable function are looked at in the order they appear in.Arezzo
S
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.

Spallation answered 26/6, 2013 at 12:36 Comment(0)
D
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.

Diffuse answered 26/6, 2013 at 8:27 Comment(7)
@DyP: There are no non-template functions here.Arezzo
@Arezzo Oh, I'm too tired... yeahDemonize
@DyP I know, but templates complicate things. I added a note to explain that.Diffuse
@PierreBdR: Implicit-conversion-sequences are handled before partial-ordering, making the first overload more viable.Arezzo
@Arezzo 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.Demonize
@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.Arezzo
@Arezzo 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" ;)Demonize

© 2022 - 2024 — McMap. All rights reserved.