C++'s Non-Static Destruction Order Fiasco

Let's just jump right in. Local variables are destructed in reverse order of construction. Every beginner learns this.

#include <cstdio>
struct LifetimePrinter
{
int id;
LifetimePrinter(int const id_) noexcept
: id{id_}
{
std::printf("[%i] int construct\n", id);
}
LifetimePrinter(LifetimePrinter const& f) noexcept
: id{f.id}
{
std::printf("[%i] copy construct\n", id);
}
LifetimePrinter(LifetimePrinter&& f) noexcept
: id{f.id}
{
std::printf("[%i] move construct\n", id);
if(f.id > 0)
{ //mark `f` as moved from
f.id = -f.id;
}
}
LifetimePrinter& operator=(LifetimePrinter const& f) noexcept
{
std::printf("[%i] copy assign from %i\n", id, f.id);
id = f.id;
return *this;
}
LifetimePrinter& operator=(LifetimePrinter&& f) noexcept
{
std::printf("[%i] move assign from %i\n", id, f.id);
int const temp{id};
id = f.id;
f.id = temp;
return *this;
}
~LifetimePrinter() noexcept
{
std::printf("[%i] destruct\n", id);
}
};
int main()
{
LifetimePrinter val1{1};
LifetimePrinter val2{2};
std::printf("hello %i and %i!\n", val1.id, val2.id);
}

Run the above program in any C++ compiler and you'll get consistent output:

[1] int construct
[2] int construct
hello 1 and 2!
[2] destruct
[1] destruct

You'll get that same output with arrays too:

int main()
{
LifetimePrinter vals[2]{1, 2};
std::printf("hello %i and %i!\n", vals[0].id, vals[1].id);
}

And with dynamically allocated arrays:

int main()
{
auto vals = new LifetimePrinter[2]{1, 2};
std::printf("hello %i and %i!\n", vals[0].id, vals[1].id);
delete[] vals;
}

Ever consistent, C++ decided that the order of data members in a class defines both the memory layout and the order of construction and destruction, with no easy way to separate the two, so you'll get the same output above for this as well:

int main()
{
struct
{
LifetimePrinter val1{1};
LifetimePrinter val2{2};
} vals;
std::printf("hello %i and %i!\n", vals.val1.id, vals.val2.id);
}

Even if you make a custom constructor and specify the data member initializers in a different order, the compiler will just warn you and re-order them to match the order of the data members, and there's no manual control over destruction. This is all basic beginner stuff everyone knows. It's a simple and easy rule to remember: construction always goes first to last, destruction always goes last to first. Look, just to prove it to you, I'll run this program in three different compilers with three different standard library implementations:

#include <vector>
int main()
{
std::vector<LifetimePrinter> vals{1, 2};
std::printf("hello %i and %i!\n", vals.at(0).id, vals.at(1).id);
}

Using clang with libc++, we get this output:

[1] int construct
[2] int construct
[1] copy construct
[2] copy construct
[2] destruct
[1] destruct
hello 1 and 2!
[2] destruct
[1] destruct

See? They always destruct in reverse order. Nice and consistent. I probably don't even have to show you the results of the other two because they'll be the same, but I'll humor you just because I'm nice. Here's GCC with libstdc++:

[1] int construct
[2] int construct
[1] copy construct
[2] copy construct
[2] destruct
[1] destruct
hello 1 and 2!
[1] destruct
[2] destruct

See? Just like I said, 2 destructs before 1 and then... wait... oh no... Well, uh, this is probably just a regression in my copy of GCC or libstdc++, I'm sure they'll fix it. Let's try MSVC with Microsoft's standard library to be the tie breaker:

[1] int construct
[2] int construct
[1] copy construct
[2] copy construct
[2] destruct
[1] destruct
hello 1 and 2!
[1] destruct
[2] destruct

It's... it's the same as GCC. No no no, this can't be right. Destruction always goes last to first. It's always last to first. Stars above... how much code have I written with this false assumption!?

Okay... okay... I've calmed down. Clearly this is just a defect in the standard. They forgot to specify the order in which containers must destruct elements. I'm sure they'll fix that in a few half decades. Let's try something that doesn't use any standard library containers that can contain multiple elements:

#include <utility>
int main()
{
struct Vals
{
LifetimePrinter val1;
LifetimePrinter val2;
};
Vals vals_a{1, 2};
std::printf("hello %i and %i!\n", vals_a.val1.id, vals_a.val2.id);
Vals vals_b{3, 4};
std::printf("so long %i and %i!\n", vals_b.val1.id, vals_b.val2.id);
vals_b = std::move(vals_a);
std::printf("hello again %i and %i!\n", vals_b.val1.id, vals_b.val2.id);
}

Here we give the compiler's autogenerated move assignment operator a little warmup. The output is perfectly normal in every compiler:

[1] int construct
[2] int construct
hello 1 and 2!
[3] int construct
[4] int construct
so long 3 and 4!
[3] move assign from 1
[4] move assign from 2
hello again 1 and 2!
[2] destruct
[1] destruct
[4] destruct
[3] destruct

No destructive move for C++ yet, alas, but the destruction always goes in the correct (reverse) order. Let's add a small layer of indirection to prove that this holds generally:

#include <memory>
#include <utility>
int main()
{
struct Vals
{
std::unique_ptr<LifetimePrinter> val1;
std::unique_ptr<LifetimePrinter> val2;
};
Vals vals_a{std::make_unique<LifetimePrinter>(1), std::make_unique<LifetimePrinter>(2)};
std::printf("hello %i and %i!\n", vals_a.val1->id, vals_a.val2->id);
Vals vals_b{std::make_unique<LifetimePrinter>(3), std::make_unique<LifetimePrinter>(4)};
std::printf("so long %i and %i!\n", vals_b.val1->id, vals_b.val2->id);
vals_b = std::move(vals_a);
std::printf("hello again %i and %i!\n", vals_b.val1->id, vals_b.val2->id);
}

Since std::unique_ptr only contains zero or one elements, there's no way it can mess up destruction order. And since the Vals type isn't in the hands of the standard library, there's no way the compiler is gonna mess up its destruct order either. Here's what we get:

[1] int construct
[2] int construct
hello 1 and 2!
[3] int construct
[4] int construct
so long 3 and 4!
[3] destruct
[4] destruct
hello again 1 and 2!
[2] destruct
[1] destruct

See? 2 destructs before 1, and 3 destructs...

...

Oh.

Literally both of the things I said couldn't go wrong did go wrong. It truns out, std::unique_ptr does not implement moves as swaps. It implements moves by destructing the current value and then transferring in the incoming value. And it's explicitly specified to do so by the standard. And, as seen earlier, the compiler-generated move assignment operator just blindly goes in forward order of the data members and invokes their move assignment operators. That's also explicitly specified by the standard.

So, either we have to make our own type that implements moves as swaps, or we have to manually define the move assignment operator and explicitly swap each data member, or explicitly destruct each data member in the correct order before transferring in the new values. I really, really don't want to manually implement the move assignment operator. Not only is it tedious when you have a lot of data members, not only does it easily get out of sync with the rest of the class, not only does it force you to define all the other special member functions too... the worst part is that if you go with the manual destruct approach, the compiler won't even help you shortcut the rest of it when it easily could.

Well, unless you engage in the time old beloved pastime of abusing inheritance in C++:

#include <memory>
#include <utility>
int main()
{
struct ValsBase
{
std::unique_ptr<LifetimePrinter> val1;
std::unique_ptr<LifetimePrinter> val2;
};
struct Vals
: ValsBase
{
Vals& operator=(Vals&& f) noexcept
{
val2.reset();
val1.reset();
ValsBase::operator=(std::move(f));
return *this;
}
};
Vals vals_a{std::make_unique<LifetimePrinter>(1), std::make_unique<LifetimePrinter>(2)};
std::printf("hello %i and %i!\n", vals_a.val1->id, vals_a.val2->id);
Vals vals_b{std::make_unique<LifetimePrinter>(3), std::make_unique<LifetimePrinter>(4)};
std::printf("so long %i and %i!\n", vals_b.val1->id, vals_b.val2->id);
vals_b = std::move(vals_a);
std::printf("hello again %i and %i!\n", vals_b.val1->id, vals_b.val2->id);
}

With that, we now get proper destruct order every time:

[1] int construct
[2] int construct
hello 1 and 2!
[3] int construct
[4] int construct
so long 3 and 4!
[4] destruct
[3] destruct
hello again 1 and 2!
[2] destruct
[1] destruct

Problem is, we broke move construction. And we can't fix it. As soon as we add any special member function, the compiler gives up on generating implicit move operations. As soon as we add any constructor at all, even a defaulted one, aggregate initialization and designated initialization both vanish. At best we can make a templated constructor that forwards all its arguments to the base class using curly braces, but that won't fix designated initialization. I even tried to abuse inheritance in C++ even more by adding a second base class using CRTP and explicit object parameters to do the real work, but it was always either invoked too late, ambiguous, or not used.

Let's review:

  • Local variables are always destructed in reverse order of construction, which is reverse order of definition.
  • Array elements are always destructed last to first, for all three of plain arrays, dynamically allocated arrays, and std::array. Except when the move assignment operator is used. Then you're at the mercy of whether the array element type implements moves as swaps or not.
  • Data members are always destructed in reverse order of definition. Except when the move assignment operator is generated by the compiler. Then you're at the mercy of whether each data member type implements moves as swaps or not.
  • Standard library containers are permitted to destruct elements in any order they like.

So what are the workarounds? Wrapper classes. Wrap the array element type to force moves to be swaps, wrap the data member types to force moves to be swaps, wrap the standard library containers to force moves to be swaps and to force destruction to go in the correct order.

That's a wrap.

Comments