std::initializer_list as function argument

27,230

Solution 1

GCC has a bug. The Standard makes this valid. See:

Notice that there are two sides of this

  • How and what initialization is done in general?
  • How is initialization used during overload resolution, and what cost does it have?

The first question is answered in section 8.5. The second question is answered in section 13.3. For example, reference binding is handled at 8.5.3 and 13.3.3.1.4, while list initialization is handled in 8.5.4 and 13.3.3.1.5.

8.5/14,16:

The initialization that occurs in the form

T x = a;

as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization.
.
.
The semantics of initializers are as follows[...]: If the initializer is a braced-init-list, the object is list-initialized (8.5.4).

When considering the candidate function, the compiler will see an initializer list (which has no type yet - it's just a grammatical construct!) as the argument, and a std::vector<std::string> as the parameter of function. To figure out what the cost of conversion is and whether we can convert these in context of overloading, 13.3.3.1/5 says

13.3.3.1.5/1:

When an argument is an initializer list (8.5.4), it is not an expression and special rules apply for converting it to a parameter type.

13.3.3.1.5/3:

Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence. User-defined conversions are allowed for conversion of the initializer list elements to the constructor parameter types except as noted in 13.3.3.1.

The non-aggregate class X is std::vector<std::string>, and i will figure out the single best constructor below. The last rule grants us to use user defined conversions in cases like the following:

struct A { A(std::string); A(A const&); };
void f(A);
int main() { f({"hello"}); }

We are allowed to convert the string literal to std::string, even if this needs a user defined conversion. However, it points to restrictions of another paragraph. What does 13.3.3.1 say?

13.3.3.1/4, which is the paragraph responsible for forbidding multiple user defined conversions. We will only look at list initializations:

However, when considering the argument of a user-defined conversion function [(or constructor)] that is a candidate by [...] 13.3.1.7 when passing the initializer list as a single argument or when the initializer list has exactly one element and a conversion to some class X or reference to (possibly cv-qualified) X is considered for the first parameter of a constructor of X, or [...], only standard conversion sequences and ellipsis conversion sequences are allowed.

Notice that this is an important restriction: If it weren't for this, the above can use the copy-constructor to establish an equally well conversion sequence, and the initialization would be ambiguous. (notice the potential confusion of "A or B and C" in that rule: It is meant to say "(A or B) and C" - so we are restricted only when trying to convert by a constructor of X having a parameter of type X).

We are delegated to 13.3.1.7 for collecting the constructors we can use to do this conversion. Let's approach this paragraph from the general side starting from 8.5 which delegated us to 8.5.4:

8.5.4/1:

List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.

8.5.4/2:

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6).

8.5.4/3:

List-initialization of an object or reference of type T is defined as follows: [...] Otherwise, if T is a class type, constructors are considered. If T has an initializer-list constructor, the argument list consists of the initializer list as a single argument; otherwise, the argument list consists of the elements of the initializer list. The applicable constructors are enumerated (13.3.1.7) and the best one is chosen through overload resolution (13.3).

At this time, T is the class type std::vector<std::string>. We have one argument (which does not have a type yet! We are just in the context of having a grammatical initializer list). Constructors are enumerated as of 13.3.1.7:

[...] If T has an initializer-list constructor (8.5.4), the argument list consists of the initializer list as a single argument; otherwise, the argument list consists of the elements of the initializer list. For copy-list-initialization, the candidate functions are all the constructors of T. However, if an explicit constructor is chosen, the initialization is ill-formed.

We will only consider the initializer list of std::vector as the only candidate, since we already know the others won't win against it or won't fit the argument. It has the following signature:

vector(initializer_list<std::string>, const Allocator& = Allocator());

Now, the rules of converting an initializer list to an std::initializer_list<T> (to categorize the cost of the argument/parameter conversion) are enumerated in 13.3.3.1.5:

When an argument is an initializer list (8.5.4), it is not an expression and special rules apply for converting it to a parameter type. [...] If the parameter type is std::initializer_list<X> and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an element of the list to X. This conversion can be a user-defined conversion even in the context of a call to an initializer-list constructor.

Now, the initializer list will be successfully converted, and the conversion sequence is a user defined conversion (from char const[N] to std::string). How this is made is detailed at 8.5.4 again:

Otherwise, if T is a specialization of std::initializer_list<E>, an initializer_list object is constructed as described below and used to initialize the object according to the rules for initialization of an object from a class of the same type (8.5). (...)

See 8.5.4/4 how this final step is made :)

Solution 2

It seems to work this way:

function( {std::string("hello"), std::string("world"), std::string("test")} );

Perhaps it is a compiler bug, but perhaps you are asking for too many implicit conversions.

Solution 3

Offhand, I'm not sure, but I suspect what's going on here is that converting to an initializer_list is one conversion, and converting that to vector is another conversion. If that's the case, you're exceeding the limit of only one implicit conversion...

Solution 4

This is either a compiler bug or your compiler doesn't support std::initializer_list. Tested on GCC 4.5.1 and it compiles fine.

Share:
27,230
fredoverflow
Author by

fredoverflow

Updated on December 08, 2020

Comments

  • fredoverflow
    fredoverflow over 3 years

    For some reason I thought C++0x allowed std::initializer_list as function argument for functions that expect types that can be constructed from such, for example std::vector. But apparently, it does not work. Is this just my compiler, or will this never work? Is it because of potential overload resolution problems?

    #include <string>
    #include <vector>
    
    void function(std::vector<std::string> vec)
    {
    }
    
    int main()
    {
        // ok
        std::vector<std::string> vec {"hello", "world", "test"};
    
        // error: could not convert '{"hello", "world", "test"}' to 'std::vector...'
        function( {"hello", "world", "test"} );
    }