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> | |
| { | |
| | |
| LifetimePrinter( | |
| : id{id_} | |
| { | |
| std::printf( | |
| } | |
| LifetimePrinter(LifetimePrinter | |
| : id{f.id} | |
| { | |
| std::printf( | |
| } | |
| LifetimePrinter(LifetimePrinter&& f) | |
| : id{f.id} | |
| { | |
| std::printf( | |
| | |
| { | |
| f.id = -f.id; | |
| } | |
| } | |
| LifetimePrinter& | |
| { | |
| std::printf( | |
| id = f.id; | |
| | |
| } | |
| LifetimePrinter& | |
| { | |
| std::printf( | |
| | |
| id = f.id; | |
| f.id = temp; | |
| | |
| } | |
| ~LifetimePrinter() | |
| { | |
| std::printf( | |
| } | |
| }; | |
| { | |
| LifetimePrinter val1{ | |
| LifetimePrinter val2{ | |
| std::printf( | |
| } |
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:
| { | |
| LifetimePrinter vals[ | |
| std::printf( | |
| } |
And with dynamically allocated arrays:
| { | |
| | |
| std::printf( | |
| | |
| } |
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:
| { | |
| | |
| { | |
| LifetimePrinter val1{ | |
| LifetimePrinter val2{ | |
| } vals; | |
| std::printf( | |
| } |
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> | |
| { | |
| std::vector<LifetimePrinter> vals{ | |
| std::printf( | |
| } |
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> | |
| { | |
| | |
| { | |
| LifetimePrinter val1; | |
| LifetimePrinter val2; | |
| }; | |
| Vals vals_a{ | |
| std::printf( | |
| Vals vals_b{ | |
| std::printf( | |
| vals_b = std::move(vals_a); | |
| std::printf( | |
| } |
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> | |
| { | |
| | |
| { | |
| std::unique_ptr<LifetimePrinter> val1; | |
| std::unique_ptr<LifetimePrinter> val2; | |
| }; | |
| Vals vals_a{std::make_unique<LifetimePrinter>( | |
| std::printf( | |
| Vals vals_b{std::make_unique<LifetimePrinter>( | |
| std::printf( | |
| vals_b = std::move(vals_a); | |
| std::printf( | |
| } |
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> | |
| { | |
| | |
| { | |
| std::unique_ptr<LifetimePrinter> val1; | |
| std::unique_ptr<LifetimePrinter> val2; | |
| }; | |
| | |
| : ValsBase | |
| { | |
| Vals& | |
| { | |
| val2.reset(); | |
| val1.reset(); | |
| ValsBase:: | |
| | |
| } | |
| }; | |
| Vals vals_a{std::make_unique<LifetimePrinter>( | |
| std::printf( | |
| Vals vals_b{std::make_unique<LifetimePrinter>( | |
| std::printf( | |
| vals_b = std::move(vals_a); | |
| std::printf( | |
| } |
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.
Comments
Post a Comment
Remember the universal code of conduct