Jo Van der Snickt
Jo Van der Snickt

Reputation: 13

Why does the initialization of an std::initializer_list seem to fail in VS2019

The following piece of code fails on Visual Studio 2019 compiled in Release mode.

#include <iostream>
#include <iterator>
#include <initializer_list>

int main( int, char** )
{
    std::initializer_list<int> v = {};
    std::initializer_list<int> i = { 1, 2, 3 };

    v = i;
    std::copy( v.begin(), v.end(), std::ostream_iterator<int>( std::cout, " " ) );
    std::cout << std::endl;

    v = { 1, 2, 3 };
    std::copy( v.begin(), v.end(), std::ostream_iterator<int>( std::cout, " " ) );
    std::cout << std::endl;
}

The second initialization of v seems to fail and the output is like:

1 2 3
17824704 10753212 0

But when building in Debug mode or building with other compilers (gcc, clang). The output is as expected:

1 2 3
1 2 3

What could be the reason for this?

Upvotes: 1

Views: 652

Answers (1)

paxdiablo
paxdiablo

Reputation: 882116

Just to be clear, the only initialisation of v takes place in the line:

std::initializer_list<int> v = {};

The other two are assignments rather than initialisation:

v = i;
v = { 1, 2, 3 };

And it's the difference between those two assignments that provides the solution.

Copying an initialiser list does not necessarily copy the underlying elements - initialiser lists are usually a lightweight thing, and are frequently implemented as just a pointer and length.

So when you assign, i to v, the underlying array of i (and therefore v) continues to exist until going out of scope at the end of main. No problems there.

When you copy {1, 2, 3} to v, the underlying array also continues to exist until it goes out of scope. Unfortunately, that happens as soon as the assignment finishes, meaning that using the elements of v will be problematic after that point.

Though v most likely will still has a pointer and length, the thing that the pointer points to has gone out of scope, and that memory may well have been reused for something else.


The relevant text in the standard (C++20 [dcl.init.list] /6) states, when talking about initialiser lists:

The [underlying] array has the same lifetime as any other temporary object, except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary.

Since what you're doing isn't initialisation, this lifetime extension does not occur.

That means the destruction of the underlying array is covered by C++20 [class.temporary] /4:

Temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created.


Interestingly, the cplusplus.com site for initializer_list currently has buggy code on it with this exact problem (I've raised the issue with them, not sure when, or even if, they'll fix it):

// initializer_list example
#include <iostream>          // std::cout
#include <initializer_list>  // std::initializer_list

int main ()
{
    std::initializer_list<int> mylist;
    mylist = { 10, 20, 30 };
    std::cout << "mylist contains:";
    for (int x: mylist) std::cout << ' ' << x;
    std::cout << '\n';
    return 0;
}

While that may work in some scenarios, it's by no means guaranteed and, in fact, gcc 10.2 (checked on Compiler Explorer) rightly picks it up as problematic:

<source>: In function 'int main()':
<source>:8:25: warning: assignment from temporary 'initializer_list'
    does not extend the lifetime of the underlying array
    [-Winit-list-lifetime]
8 |   mylist = { 10, 20, 30 };
  |                         ^

Upvotes: 6

Related Questions