4

It looks like when I create a std::initializer_list<B*> where class B is derived from class A and pass this to a function which accepts a std::initializer_list<A*>, the compiler gets confused. If, however, I create a std::initializer_list<B*> in-place with brace initializers (which I'd assume makes a temporary std::initializer_list) it's able to convert it just fine.

Specifically it seems to be unable to convert std::initializer_list<B*> to std::initializer_list<A*>, even though some conversion clearly exists as evidenced by the one working function call here.

What's the explanation for this behavior?

#include <initializer_list>
#include <iostream>


class A {
public:
    A() {}
};

class B : public A {
public:
    B() {}
};

using namespace std;

void do_nothing(std::initializer_list<A*> l) {

}

int main() {
    B* b1;
    B* b2;

    std::initializer_list<B*> blist = {b1, b2};


    //error, note: candidate function not viable: no known conversion from 
    //'initializer_list<B *>' to 'initializer_list<A *>' for 1st argument

    //do_nothing(blist);

    //Totally fine, handles conversion.
    do_nothing({b1, b2});

    return 0;
}

Try it here.

edit:

As a workaround doing something like this std::initializer_list<A*> alist = {b1, b2}; seems to be accepted by do_nothing() but I'm still curious about the behavior.

brenzo
  • 677
  • 2
  • 6
  • 11

3 Answers3

5

The reason for this is that the initializer list here

do_nothing({b1, b2});

is of a different type than

std::initializer_list<B*> blist = {b1, b2};

Since do_nothing takes a std::initializer_list<A*> the braced initialization list in your function call (do_nothing({b1, b2})) is used to construct the std::initializer_list<A*> from your function parameter. This works, because B* is implicitly convertible to A*. However, std::initializer_list<B*> is not implicitly convertible to std::initializer_list<A*>, hence you get that compiler error.

Lets write some pseudo-code to demonstrate what happens. First we take a look at the working part of the code:

do_nothing({b1, b2});  // call the function with a braced-init-list

// pseudo code starts here

do_nothing({b1, b2}):                       // we enter the function, here comes our braced-init-list
   std::initializer_list<A*> l {b1, b2};    // this is our function parameter that gets initialized with whatever is in that braced-init-list
   ...                                      // run the actual function body

and now the one that doesn't work:

std::initializer_list<B*> blist = {b1, b2}; // creates an actual initializer_list
do_nothing(blist);                          // call the function with the initializer_list, NOT a braced-init-list

// pseudo code starts here

do_nothing(blist):                      // we enter the function, here comes our initializer_list
   std::initializer_list<A*> l = blist; // now we try to convert an initializer_list<B*> to an initializer_list<A*> which simply isn't possible
   ...                                  // we get a compiler error saying we can't convert between initializer_list<B*> and initializer_list<A*>

Note the terms braced-init-list and initializer_list. While looking similar, those are two very different things.

A braced-init-list is a pair of curly braces with values in between, something like this:

{ 1, 2, 3, 4 }

or this:

{ 1, 3.14, "different types" }

it is a special construct used for initialization that has its own rules in the C++ language.

On the other hand, std::initializer_list is just a type (actually a template but we ignore that fact here as it doesn't really matter). From that type you can create an object (like you did with your blist) and initialize that object. And because braced-init-list is a form of initialization we can use it on the std::initializer_list:

std::initializer_list<int> my_list = { 1, 2, 3, 4 };

Because C++ has a special rule that allows us to initialize each function argument with a braced-init-list, do_nothing({b1, b2}); compiles. This also works for multiple arguments:

void do_something(std::vector<int> vec, std::tuple<int, std::string, std::string> tup) 
{ 
    // ...
}

do_something({1, 2, 3, 4}, {10, "first", "and 2nd string"});

or nested initialization:

void do_something(std::tuple<std::tuple<int, std::string>, std::tuple<int, int, int>, double> tup) 
{ 
    // ...
}

do_something({{1, "text"}, {2, 3, 4}, 3.14});
Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Timo
  • 9,269
  • 2
  • 28
  • 58
4

We must distinguish between a std::initializer_list<T> object, and a braced-init-list.

Braced-init-lists are enclosed by braces, separated by commas, and are a source-level construct. They can be found in various contexts and their elements do not all need to be the same type. For example

std::pair<int, std::string> = {47, "foo"};  // <- braced-init-list

When a statement that contains a braced-init-list is executed, sometimes a std::initializer_list<T> object (for some T) is created from the elements of the braced-init-list. Those contexts are:

  • When a variable declared with type std::initializer_list<T> has a braced-init-list as its initializer; and
  • When an object is declared with auto type and is copy-initialized from a nonempty braced-init-list.

If the std::initializer_list<T> object cannot be created from the braced-init-list, a compilation error occurs. For example

std::initializer_list<int> l = {47, "foo"};  // error

In addition to the above ways of creating a std::initializer_list object, there is one additional way: by copying an existing one. This is a shallow copy (it just makes the new object point to the same array as the old one). A copy, of course, does not change the type.


Now back to your code. When you try to call the do_nothing function with the argument blist, you are trying to do something impossible because what you have provided is not something that can be used to create an std::initializer_list<A*> object. The only way to create such an object would be from a braced-init-list or from an existing std::initializer_list<A*> object.

However, passing {b2, b1} as the argument works just fine, because it is a braced-init-list. Implicit conversions are allowed, from the elements of the braced-init-list to the required element type.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
1

std::initializer_list is a template.

Generally in C++, some_template<T> is unrelated to some_template<U> even if T is related to U.

In the call do_something({b1, b2}) the compiler actually creates std::initializer_list<A*> (after applying list-initialization rules).

You can templatize do_nothing to take different types:

template<typename T, typename = std::enable_if_t<std::is_convertible<T, A>::value>>
void do_nothing(std::initializer_list<T*> l) {

}

(the optional SFINAE part restricts T to types convertible to A)

For more possible solutions refer to this related question.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
rustyx
  • 80,671
  • 25
  • 200
  • 267