Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

lifetime of a std::initializer_list return value

GCC's implementation destroys a std::initializer_list array returned from a function at the end of the return full-expression. Is this correct?

Both test cases in this program show the destructors executing before the value can be used:

#include <initializer_list> #include <iostream>  struct noisydt {     ~noisydt() { std::cout << "destroyed\n"; } };  void receive( std::initializer_list< noisydt > il ) {     std::cout << "received\n"; }  std::initializer_list< noisydt > send() {     return { {}, {}, {} }; }  int main() {     receive( send() );     std::initializer_list< noisydt > && il = send();     receive( il ); } 

I think the program should work. But the underlying standardese is a bit convoluted.

The return statement initializes a return value object as if it were declared

std::initializer_list< noisydt > ret = { {},{},{} }; 

This initializes one temporary initializer_list and its underlying array storage from the given series of initializers, then initializes another initializer_list from the first one. What is the array's lifetime? "The lifetime of the array is the same as that of the initializer_list object." But there are two of those; which one is ambiguous. The example in 8.5.4/6, if it works as advertised, should resolve the ambiguity that the array has the lifetime of the copied-to object. Then the return value's array should also survive into the calling function, and it should be possible to preserve it by binding it to a named reference.

On LWS, GCC erroneously kills the array before returning, but it preserves a named initializer_list per the example. Clang also processes the example correctly, but objects in the list are never destroyed; this would cause a memory leak. ICC doesn't support initializer_list at all.

Is my analysis correct?


C++11 §6.6.3/2:

A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.

8.5.4/1:

… list-initialization in a copy-initialization context is called copy-list-initialization.

8.5/14:

The initialization that occurs in the form T x = a; … is called copy-initialization.

Back to 8.5.4/3:

List-initialization of an object or reference of type T is defined as follows: …

— 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).

8.5.4/5:

An object of type std::initializer_list<E> is constructed from an initializer list as if the implementation allocated an array of N elements of type E, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and the std::initializer_list<E> object is constructed to refer to that array. If a narrowing conversion is required to initialize any of the elements, the program is ill-formed.

8.5.4/6:

The lifetime of the array is the same as that of the initializer_list object. [Example:

typedef std::complex<double> cmplx;  std::vector<cmplx> v1 = { 1, 2, 3 };  void f() {    std::vector<cmplx> v2{ 1, 2, 3 };    std::initializer_list<int> i3 = { 1, 2, 3 };  } 

For v1 and v2, the initializer_list object and array createdfor { 1, 2, 3 } have full-expression lifetime. For i3, the initializer_list object and array have automatic lifetime. — end example]


A little clarification about returning a braced-init-list

When you return a bare list enclosed in braces,

A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.

This doesn't imply that the object returned to the calling scope is copied from something. For example, this is valid:

struct nocopy {     nocopy( int );     nocopy( nocopy const & ) = delete;     nocopy( nocopy && ) = delete; };  nocopy f() {     return { 3 }; } 

this is not:

nocopy f() {     return nocopy{ 3 }; } 

Copy-list-initialization simply means the equivalent of the syntax nocopy X = { 3 } is used to initialize the object representing the return value. This doesn't invoke a copy, and it happens to be identical to the 8.5.4/6 example of an array's lifetime being extended.

And Clang and GCC do agree on this point.


Other notes

A review of N2640 doesn't turn up any mention of this corner case. There has been extensive discussion about the individual features combined here, but I don't see anything about their interaction.

Implementing this gets hairy as it comes down to returning an optional, variable-length array by value. Because the std::initializer_list doesn't own its contents, the function has to also return something else which does. When passing to a function, this is simply a local, fixed-size array. But in the other direction, the VLA needs to be returned on the stack, along with the std::initializer_list's pointers. Then the caller needs to be told whether to dispose of the sequence (whether they're on the stack or not).

The issue is very easy to stumble upon by returning a braced-init-list from a lambda function, as a "natural" way to return a few temporary objects without caring how they're contained.

auto && il = []() -> std::initializer_list< noisydt >                { return { noisydt{}, noisydt{} }; }(); 

Indeed, this is similar to how I arrived here. But, it would be an error to leave out the -> trailing-return-type because lambda return type deduction only occurs when an expression is returned, and a braced-init-list is not an expression.

like image 785
Potatoswatter Avatar asked Mar 08 '13 03:03

Potatoswatter


People also ask

What is std :: initializer_list?

An object of type std::initializer_list<T> is a lightweight proxy object that provides access to an array of objects of type const T .

What is uniform initialization in C++?

Uniform initialization is a feature in C++ 11 that allows the usage of a consistent syntax to initialize variables and objects ranging from primitive type to aggregates. In other words, it introduces brace-initialization that uses braces ({}) to enclose initializer values.


1 Answers

std::initializer_list is not a container, don't use it to pass values around and expect them to persist

DR 1290 changed the wording, you should also be aware of 1565 and 1599 which aren't ready yet.

Then the return value's array should also survive into the calling function, and it should be possible to preserve it by binding it to a named reference.

No, that doesn't follow. The array's lifetime doesn't keep being extended along with the initializer_list. Consider:

struct A {     const int& ref;     A(const int& i = 0) : ref(i) { } }; 

The reference i binds to the temporary int, and then the reference ref binds to it as well, but that doesn't extend the lifetime of i, it still goes out of scope at the end of the constructor, leaving a dangling reference. You don't extend the underlying temporary's lifetime by binding another reference to it.

Your code might be safer if 1565 is approved and you make il a copy not a reference, but that issue is still open and doesn't even have proposed wording, let alone implementation experience.

Even if your example is meant to work, the wording regarding lifetime of the underlying array is obviously still being improved and it will take a while for compilers to implement whatever final semantics are settled on.

like image 131
Jonathan Wakely Avatar answered Sep 25 '22 20:09

Jonathan Wakely