C++11: In what order are lambda captures destructed?
Asked Answered
G

4

15

Let's say I have two local smart pointers, foo and bar.

shared_ptr<Foo> foo = ...
shared_ptr<Bar> bar = ...

These smart pointers are wrappers around resources that for some reason must be destructed in the order foo, then bar.

Now I want to create a lambda that uses foo and bar, but outlives the scope containing them. So I'd capture them by value, like this:

auto lambda = [foo, bar]() { ... };

This creates copies of foo and bar within the function object. When the function object is destructed, these copies will be destructed, as well, but I care about the order in which this happens. So my question is:

When a lambda object is destructed, in what order are its by-value captures destructed? And how can I (hopefully) influence this order?

Grad answered 20/9, 2012 at 20:41 Comment(3)
I think it'd be interesting to also consider [=].Darla
@R.MartinhoFernandes: [foo,bar] is equivalent to [=foo,=bar] i.e. it is a copy.Perhaps
@David : I think he literally meant [=], i.e. consider what the declaration order would be without listing the variables one's self. (Obviously it's a moot point now since the declaration order is unspecified regardless of how ones does the captures.)Gate
L
20

The spec covers this... sort of. From 5.1.2, paragraph 14:

An entity is captured by copy if it is implicitly captured and the capture-default is = or if it is explicitly captured with a capture that does not include an &. For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified.

Emphasis added. Because the declaration order is unspecified, the construction order is unspecified (since the order of construction is the same as the order of declaration). And therefore, the destruction order is unspecified, since the order of destruction is the reverse of the order of construction.

In short, if you need to care about declaration order (and the various construction/destruction orders that key off of it), you cannot use a lambda. You'll need to make your own type.

Laicize answered 20/9, 2012 at 20:57 Comment(5)
Couldn't you reset one of the shared pointers at the end of your lambda, in order to ensure it releases its resource then instead of during destruction?Fasces
Nevermind; see my answer. The lambda has to be mutable for that to work.Fasces
@Nicol: Thanks for the clarification. I'm a bit puzzled that they didn't specify an order - usually, in C++, about everything concerning construction and destruction order is well-specified. But at least they specified it's unspecified, so one won't rely on the order a specific compiler happens to use.Grad
How unfortunate. I'm surprised this is not defined. Did they just run out of time and decide to put off deciding this until some future version of C++? Is this likely to be addressed in C++14?Ation
@DanielWolf The evaluation order of function call arguments is also unspecified, which dates back to those old C days, when it was efficient for compilers to push the last argument on the stack, first.Sarabia
F
11

As Nicol says, the order of destruction is unspecified.

However, you shouldn't have to depend on the destruction of the lambda. You should be able to simply reset foo at the end of your lambda, thus ensuring it releases its resource before bar does. You'll also have to mark the lambda as mutable though. The only downside here is you can't call the lambda multiple times and expect it to work.

auto lambda = [foo, bar]() mutable { ...; foo.reset(); };

If you do need your lambda to be callable multiple times, then you need to come up with some other way to control the order of deallocation. One option would be to use an intermediate structure with a known data member order, such as a std::pair<>:

auto p = std::make_pair(bar, foo);
auto lambda = [p]() { auto foo = p.second, bar = p.first; ... };
Fasces answered 20/9, 2012 at 21:33 Comment(1)
In my case, the lambda is indeed guaranteed to be executed only once, so your manual reset approach should work fine. I forgot that in order to release what a shared_ptr points to, you needn't necessarily destuct it. Thanks a lot!Grad
R
8

Rather than worrying on what the order of destruction will be, you should fix the fact that this is a problem. Noting that you are using shared pointers for both objects, you can ensure the order of destruction by adding a shared pointer in the object that you need to outlive the other. At that point whether foo or bar is destroyed earlier will not matter. If the order is correct, destruction of the shared pointer will release the objects immediately. If the order is incorrect the additional shared pointer will maintain the object alive until the other goes away.

Repository answered 20/9, 2012 at 21:31 Comment(0)
L
5

According to the C++11 document I have (i.e. the freebie, slightly-prior-to-ratification n3242), section 5.1.2, para 21, the captures are constructed in declaration order and destructed in reverse declaration order. However, declaration order is unspecified (paragraph 14). So the answer is, "in unspecified order" and "you cannot influence it" (except, I suppose, by writing a compiler).

If bar really needs to be destructed before foo, it would be wise for bar to hold a shared pointer to foo (or something of the kind).

Like answered 20/9, 2012 at 21:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.