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?

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Daniel Wolf
  • 12,855
  • 13
  • 54
  • 80

4 Answers4

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 2
    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? – Lily Ballard Sep 20 '12 at 21:30
  • 1
    Nevermind; see my answer. The lambda has to be mutable for that to work. – Lily Ballard Sep 20 '12 at 21:43
  • 1
    @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. – Daniel Wolf Sep 21 '12 at 06:28
  • 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? – allyourcode Sep 02 '14 at 03:19
  • @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. – Kai Petzke Mar 30 '23 at 19:29
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; ... };
Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • 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! – Daniel Wolf Sep 21 '12 at 06:36
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.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
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).

rici
  • 234,347
  • 28
  • 237
  • 341