Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can my compiler not figure out this conversion, when does it exist?

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.

like image 707
brenzo Avatar asked Jan 26 '23 09:01

brenzo


1 Answers

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});
like image 76
Timo Avatar answered May 10 '23 21:05

Timo