lifetime of a std::initializer_list return value

c++, c++11, initializer-list, lifetime, list-initialization

Solution

The wording you refer to in 8.5.4/6 is defective, and was corrected (somewhat) by DR1290. Instead of saying:

The lifetime of the array is the same as that of the `initializer_list` object.

... the amended standard now says:

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

Therefore the controlling wording for the lifetime of the temporary array is 12.2/5, which says:

The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement

Therefore the `noisydt` objects are destroyed before the function returns.

Until recently, Clang had a bug that caused it to fail to destroy the underlying array for an `initializer_list` object in some circumstances. I've fixed that for Clang 3.4; the output for your test case from Clang trunk is:

destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received

... which is correct, per DR1290.

Problem

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.

Original source