52

Is there a rule which states in which order the members of an std::tuple are destroyed?

For example if Function1 returns an std::tuple<std::unique_ptr<ClassA>, std::unique_ptr<ClassB>> to Function2, then can I be sure that (when the scope of Function2 is left) the instance of ClassB referred to by the second member is destroyed before the instance of ClassA referred to by the first member?

std::tuple< std::unique_ptr< ClassA >, std::unique_ptr< ClassB > > Function1()
{
    std::tuple< std::unique_ptr< ClassA >, std::unique_ptr< ClassB > > garbage;
    get<0>(garbage).reset( /* ... */ );
    get<1>(garbage).reset( /* ... */ );
    return garbage;
}

void Function2()
{
    auto to_be_destroyed = Function1();
    // ... do something else

    // to_be_destroyed leaves scope
    // Is the instance of ClassB destroyed before the instance of ClassA?
}
Barmar
  • 741,623
  • 53
  • 500
  • 612
z32a7ul
  • 3,695
  • 3
  • 21
  • 45
  • 2
    I am guessing it mostly depends on how `std::tuple` is implemented in your standard library. – Arunmu Aug 21 '16 at 20:28
  • 3
    I can't find anything in the spec that specifies the order of destruction for `std::tuple`. Probably should be filed as unspecified. – 101010 Aug 21 '16 at 20:46
  • 1
    http://stackoverflow.com/a/27663655/576911 – Howard Hinnant Aug 21 '16 at 22:52
  • 2
    This issue has very recently come up in Antony Polukhin's excellet CPPCon 2016 [talk](https://www.youtube.com/watch?v=abdeAew3gmQ) on achieving partial reflection in C++14 without proper language support for it. IIRC he says the only scary thing he had to do reimplement std::tuple to enforce a certain order of initialization. Also, ping @101010 on this. – einpoklum Oct 16 '16 at 15:30

3 Answers3

61

I'll offer a life lesson I've learned, rather than a direct answer, in response to your question:

If you can formulate, for multiple alternatives, a reasonable argument for why that alternative should be the one mandated by the standard - then you should not assume any of them is mandated (even if one of them happens to be).

In the context of tuples - please, please be kind to the people maintaining your code and do not allow the destruction order of tuple elements to potentially mess up the destruction of other elements. That's just evil... imagine the hapless programmer who will need to debug this thing. In fact, that poor soul might be yourself in a few years, when you've already forgotten about your clever trick from back-in-the-day.

If you absolutely must rely on destruction order, perhaps you should just be using a proper class with the tuple's elements as its data members (which you could write a destructor for, making it clear what needs to happen in what order), or some other arrangement facilitating a more explicit control of the destruction.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 2
    This is a really important lesson! When programming in C++, this tip has always to be on developers minds. – hbobenicio Aug 22 '16 at 14:31
  • I don't quite follow this. Since C++11 strings, are required to have contiguous storage. But there's a reasonable argument (the one used when C++03 was written) not to require this. Or, if you dispute that example, pick another where the standard made a judgment call that could reasonably go either way. So should I not write code that relies on things that C++11 or C++14 tightened up? Or do you mean that if the standard isn't clear, then don't rely on the next programmer to have spit hairs the same way you did? – Steve Jessop Aug 22 '16 at 15:43
  • Or for an intentionally silly example, there's a reasonable argument that `std::vector` should be called `std::dynamic_array`, and `std::valarray` should be called `std::vector`. But I think it's very reasonable to write code on the basis that `std::vector` is what the standard happens to say it is: we can't use it without assuming that. So I don't think I've understood what situations your advice applies to ;-) – Steve Jessop Aug 22 '16 at 15:46
  • @SteveJessop: About the strings example - note that all code that does not assume strings have contiguous storage works in C++03 as well (ok, assuming it doesn't rely on C++11 features of course). And if you want to rely on contiguous storage, then do so because there's a good reason. Think of how little the code `` assumes, and how widely applicable it is. Still ok, I concede you could prefix my life lesson with "All other things being equal...". – einpoklum Sep 02 '16 at 19:04
  • @SteveJessop: As for your silly example, obviously you can't not make assumptions about the API of the library you're using. But if you saw a co-worker of yours had written a file named "vector.hpp", I would not rush to assume exactly what kind of class s/he has in there. – einpoklum Sep 02 '16 at 19:05
  • 2
    As an addendum: If you _do_ end up relying on a clever trick like this, please, _please_ document **exactly** what you are doing with comments. Describe the behaviour you're exploiting, and _why_ you're exploiting it, in as much detail as you can, so that anyone who has to maintain it knows what they're dealing with. – Justin Time - Reinstate Monica Oct 16 '16 at 16:10
38

The standard doesn't specify the order of destruction for std::tuple. The fact that §20.4.1/p1 specifies that:

An instantiation of tuple with two arguments is similar to an instantiation of pair with the same two arguments.

Similar here is not interpreted as identical and consequently it's not implied that std::tuple should have a reverse destruction order of its arguments.

Given the recursive nature of std::tuple most probable is that the order of destruction is in order with the order of its arguments.

I also base my assumptions on a bug report for GCC BUG 66699 where in the discussion my assumptions above are justified.

That said, the order of destruction for std::tuple is unspecified.

101010
  • 41,839
  • 11
  • 94
  • 168
16

With Clang 3.4 I get the same destruction order for both std::pair and 2 element std::tuple and with g++ 5.3 I get opposite order which could be mainly due to the recursive implementation of std::tuple in libstd++.

So, it basically boils down to what I said in the comment, it is implementation defined.

From the BUG report:

Comment by Martin Sebor

Since the layout of std::pair members is fully specified, so is the order of their initialization and destruction. The output of the test case reflects this order.

The order of initialization (and destruction) of std:stuple subobjects is less clearly specified. At least it's not immediately obvious from my reading of the spec if any particular order is required.

The reason why the output for std::tuple with libstdc++ is the reverse of std::pair is because the implementation, which relies on recursive inheritance, stores and constructs tuple elements in the reverse order: i.e., the base class, which stores the last element, is stored and constructed first, followed by each derived class (each of which stores the last - Nth element).

The quote from standard [section 20.4.1] which the bug reporter is quoting to

1 This subclause describes the tuple library that provides a tuple type as the class template tuple that can be instantiated with any number of arguments. Each template argument specifies the type of an element in the tuple. Consequently, tuples are heterogeneous, fixed-size collections of values. An instantiation of tuple with two arguments is similar to an instantiation of pair with the same two arguments. See 20.3.

Argument against this made in the linked bug is:

Being described as similar doesn't imply they are identical in every detail. std::pair and std::tuple are distinct classes with different requirements on each. If you believe the are required to behave identically in this respect (i.e., have their subobjects defined in the same order) you need to point to the specific wording that guarantees it.

Arunmu
  • 6,837
  • 1
  • 24
  • 46