37

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.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • Aren't the 'destroyed' messages generated by GCC _before_ the `receive` call occurs simply a manifestation of the objects _inside_ the `send` function being destroyed? You pass by value, after all. In that case, this wouldn't be erroneous. Clang may optimize this away. – jogojapan Mar 08 '13 at 04:52
  • I added some more `std::cout` to the LWS example. [Weird Output](http://liveworkspace.org/code/2ZlWsj$4). I was expecting 6 `destroyed` before `----1`: 3 before `received` and 3 after it . +1 for the question. – Nawaz Mar 08 '13 at 05:06
  • @jogojapan I added output to the copy constructor but neither implementation calls it. I don't think there's any room for copy construction of `noisydt` here. Note that copying an initializer list doesn't copy the underlying array. – Potatoswatter Mar 08 '13 at 05:09
  • [Still Weird Output](http://liveworkspace.org/code/2ZlWsj$9). Where there is no `destroyed` after **first** `received` but before `----1`? – Nawaz Mar 08 '13 at 05:11
  • @Nawaz Because it's destroyed the entire array; there is nothing left to destroy. No copy. In the wild, "receive" produced a segfault because the destroyed object was a `std::string`. – Potatoswatter Mar 08 '13 at 05:13
  • @Potatoswatter: I was suspecting the same.That is a bug in the compiler? There should be at least a copy to avoid it, right? – Nawaz Mar 08 '13 at 05:17
  • @Potatoswatter You are right. Copying an initializer list does not copy the elements it contains. But that means, returning it from a function by copy is illegal, doesn't it? Because the paragraph you quoted clearly says, the lifetime ends at the end of the function, and so does the lifetime of the array (and anything it contains). – jogojapan Mar 08 '13 at 05:25
  • @jogojapan and Nawaz No copies are specified by the language except for by 8.5.4/3 and /5 (which applies only to the elements, not the list). Although it's copy-list-initialization, it's not "returning by copy." The same copy occurs in the example despite that lifetime being prolonged. The object initialized by the copy-list-initialization is the returned object which lives in the calling scope. `return std::initializer_list{ … };` would be a different story, because then the list-initialization would apply to an additional temporary, whose lifetime would determine that of the array. – Potatoswatter Mar 08 '13 at 06:00
  • @Potatoswatter I am not sure if I can follow. Your `send()` function is of the form `A send() { return {} }`, and that means a copy of `A` is performed. There may be RVO, but, nevertheless, if `A` is a complex object that contains anything local that isn't copied by the copy constructor for `A`, then you get undefined behaviour. That is precisely what is the case for `std::initializer_list`, unless there is something I've overlooked. – jogojapan Mar 08 '13 at 06:12
  • @jogojapan No RVO. `return {}` is different from `return A{}`. The former list-initializes the return value object. The latter copy-initializes the return value object using a list-initialized temporary. – Potatoswatter Mar 08 '13 at 06:15
  • @Potatoswatter You mean because of 6.6.3/2 (last sentence)? It says _`return {}` initializes the object or reference to be returned [...]_. "To be returned": I interpret that to mean the object is initialized _before it is returned_, but actually returning it still causes a copy to be made (except for RVO). If what you are saying is right, `return {}` would not even require a copy constructor to exist, correct? – jogojapan Mar 08 '13 at 06:32
  • @jogojapan Yep, I was writing up an example of exactly that as you posted. See update in Q. – Potatoswatter Mar 08 '13 at 06:38
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/25825/discussion-between-jogojapan-and-potatoswatter) – jogojapan Mar 08 '13 at 06:40
  • @Potatoswatter as confirmed in chat, you seem to be right. – jogojapan Mar 08 '13 at 06:48
  • @jogo yes no copy ctor required. that was subject of one of my quiz questions :-) – Johannes Schaub - litb Mar 08 '13 at 11:07
  • @JohannesSchaub-litb initializer lists never cease to amaze me. – jogojapan Mar 08 '13 at 11:20

2 Answers2

25

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.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • The array's lifetime is identical to the `initializer_list` (8.5.4/6). The `initializer_list` is the return value object (6.6.3/2). That object is bound to a named reference and its lifetime is extended, same as the ScopeGuard idiom (12.2/5). Therefore the array's lifetime is extended. Your counterexample is one of the exceptions listed in 12.2/5. (Surely you know this.) 1290 appears to preserve the behavior because the array is still initializing/bound to the original temporary object visible in the calling scope. 1599 gets right to the point of this question but doesn't propose a change. – Potatoswatter Mar 08 '13 at 23:48
  • 1
    Doesn't the third bullet of 12.2/5 apply to your case though? The array temporary is bound to the returned `initializer_list`, but destroyed at the end of the full expression, i.e. before you call `receive(il)`. The constructor case is one example where "rebinding" doesn't "reextend" the lifetime, and (I believe) yours is another case. I'm happy to be proved wrong though, but I try to avoid doing such things with `initializer_list` objects because I don't trust them to do what you want :) – Jonathan Wakely Mar 09 '13 at 00:35
  • No, that refers to returning a reference. `obj const &f() { return obj(); }` returns a dangling reference according to that rule, but would otherwise be valid. My case is identical to the ScopeGuard idiom. The function return object *is* the temporary; there is not a temporary bound to it. The only way to perfect the language is to push the envelope… I don't trust this because of 1599, or my alternative reading of an "ambiguity" in the Standard. Maybe I'll try adding support to GCC, it would be a good challenge but it almost certainly won't fit into the ABI (or maybe with a heap cheat). – Potatoswatter Mar 10 '13 at 00:11
  • 1
    THe `initializer_list` is the function return object, the underlying array is the temporary – Jonathan Wakely Mar 10 '13 at 12:46
  • How does that follow? The array's status is in a sort of limbo. All we know is that its lifetime is identical to the `initializer_list`. – Potatoswatter Mar 12 '13 at 00:41
21

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.

Richard Smith
  • 13,696
  • 56
  • 78
  • Excellent. I think it would be clearer if it were worded as "extends the lifetime of the array as if the `initializer_list` were of reference-to-array type." Equating the list object with a reference is sort of jumping through a mental hoop. Anyway, the whole thing is broken. The name of an `initializer_list` and its `*begin()` and `*end()` should be permanently be rvalues, and if the user wants one to persist, they should declare it `const` so typical move constructors aren't selected. – Potatoswatter Jul 14 '13 at 04:10
  • 1
    So the only use case for a function returning `initializer_list` is to return one that is `static` ? – M.M Feb 28 '15 at 00:13
  • 1
    @MattMcNabb: You could also reasonably return one that you were given as an argument. But returning an `initializer_list` should be regarded with suspicion. – Richard Smith Mar 03 '15 at 01:16
  • @RichardSmith right. I was wondering if maybe it should have been prohibited entirely since the valid use-cases are esoteric. Common compilers don't warn about this problem but perhaps they should. – M.M Mar 03 '15 at 01:19
  • 2
    Update: As of approximately last week (late July 2018), Clang trunk now warns about this, with a "returning address of local temporary object" error. – Brooks Moses Jul 30 '18 at 23:19