I've been struggling with a destructor call order which I cannot really understand.
Say we have the following definitions:
#include <memory>
#include <iostream>
class DummyClass {
std::string name;
public:
DummyClass(std::string name) : name(name) { std::cout << "DummyClass(" << name << ")" << std::endl; }
~DummyClass() { std::cout << "~DummyClass(" << name << ")" << std::endl; }
};
class TestClass {
private:
static DummyClass dummy;
static DummyClass& objects() {
static DummyClass dummy("inner");
return dummy;
}
public:
TestClass() {
std::cout << "TestClass" << std::endl;
std::cout << "TestClass Objects is: " << &objects() << std::endl;
}
virtual ~TestClass() {
std::cout << "~TestClass Objects is: " << &objects() << std::endl;
std::cout << "~TestClass" << std::endl;
}
};
DummyClass TestClass::dummy("outer");
Now, If I instantiate the TestClass as follows:
TestClass *mTest = nullptr;
int main() {
mTest = new TestClass(); delete mTest;
return 0;
}
The output obtained is the one I would expect:
DummyClass(outer)
TestClass
DummyClass(inner)
TestClass Objects is: 0x....
~TestClass Objects is: 0x....
~TestClass
~DummyClass(inner)
~DummyClass(outer)
But, now, if I use a shared_ptr for mTest, like:
std::shared_ptr<TestClass> mTest;
int main() {
mTest = std::make_shared<TestClass>();
return 0;
}
the output produced is:
DummyClass(outer)
TestClass
DummyClass(inner)
TestClass Objects is: 0x....
~DummyClass(inner)
~TestClass Objects is: 0x....
~TestClass
~DummyClass(outer)
Can someone explain why is the DummyClass inner object being destroyed before the end of the TestClass object destructor, in this particular case? I found consistent behavior for gcc 5.2.0 using -std=gnu++11 and clang 3.8.0 with -std=c++11 but could not find any particular documentation citing this example.
Edit: To clarify: all of the code above was written in the same translation unit (*.cpp file) in the presented order. It is a simplification of a usage case where I have a header only class definition which must hold a static list of this pointers to derived class objects. These pointers are added via ctor and removed when the dtor is reached. The problem is triggered when destroying the last object. The list is kept inside a static method and accessed through it to achieve the header only goal.
The rules for all objects with static storage duration (namespace members, static class members, and static objects in function definitions) are:
If the entire initialization can be considered a constant expression, that initialization happens before anything else. (Doesn't apply to anything in your examples.) Otherwise,
Namespace members and static class members are guaranteed to begin initialization at some point before any function in the same translation unit is called. (In most implementations, if we ignore dynamic library loading, all of these happen before main begins. In your examples, since main is in the same TU, we know they happen before main.)
Namespace members and static class members defined in the same TU begin their initializations in the order of their definitions.
For namespace members and static class members defined in different TUs, there is no guarantee on order of initialization!
Static objects defined inside a function begin their initialization the first time program control reaches the definition (if ever).
When main returns or std::exit is called, all objects with static storage duration are destroyed in order opposite to when each completed its initialization.
So in your second example:
Initialization of TestClass::dummy begins. First a temporary std::string is created, and then DummyClass::DummyClass(std::string) is called.
The DummyClass constructor does a std::string copy, then outputs "DummyClass(outer)\n". The temporary std::string is destroyed. Initialization of TestClass::dummy is complete.
Initialization of ::mTest begins. This calls std::shared_ptr<TestClass>::shared_ptr().
The shared_ptr constructor sets up the smart pointer to be null. Initialization of ::mTest is complete.
main begins.
The std::make_shared call ends up creating a TestClass object, calling TestClass::TestClass(). This constructor first prints "TestClass\n", then calls TestClass::objects().
Inside TestClass::objects(), initialization of local object dummy begins. Again a temporary std::string is created, and DummyClass::DummyClass(std::string) is called.
The DummyClass constructor does a std::string copy, then outputs "DummyClass(inner)\n". The temporary std::string is destroyed. Initialization of objects' dummy is complete.
TestClass::TestClass() continues, printing "TestClass Objects is: 0x...\n". Initialization of the dynamic TestClass object is complete.
Back in main, the make_shared function returns a temporary std::shared_ptr<TestClass>. A move assignment moves from the returned temporary to ::mTest, then the temporary is destroyed. Note that although the TestClass object is associated with ::mTest, it has dynamic storage duration, not static storage duration, so the above rules do not apply to it.
main returns. C++ begins destroying objects with static storage duration.
The last static object to finish initialization was the dummy local of TestClass::objects() at step 8 above, so it is destroyed first. Its destructor body outputs "~DummyClass(inner)\n".
The next object to finish initializing was ::mTest in step 4 above, so its destruction begins next. The ~shared_ptr destructor ends up destroying the owned dynamic TestClass object.
The TestClass::~TestClass() destructor body first calls TestClass::objects().
In TestClass::objects(), we encounter the definition of an already destroyed function-local static, which is Undefined Behavior! Apparently though, your implementation does nothing but return a reference to the storage that formerly contained dummy, and it's probably a good thing you didn't do anything with it other than take the address.
TestClass::~TestClass() continues, outputting "~TestClass Objects is: 0x...\n" and then "~TestClass\n".
The ~shared_ptr destructor for ::mTest deallocates associated memory and completes.
Finally, the first static object to finish initialization was TestClass::dummy, in step 2 above, so it is destroyed last. The DummyClass::~DummyClass destructor body outputs "~DummyClass\n". The program is finished.
So the big difference between your two examples is the fact that the TestClass destruction gets delayed until the shared_ptr is destroyed - it doesn't really matter when in the scheme of things the TestClass was created. Since the shared_ptr was created before the "inner" DummyClass in the second example, its destruction happens after the "inner" object is gone, causing that Undefined Behavior.
If this is a simplification of an actual issue you ran into and need to fix, you might try adding something like
class TestClass {
// ...
public:
class ForceInit {
ForceInit() { TestClass::objects(); }
};
// ...
};
// ...
TestClass::ForceInit force_init_before_mTest;
std::shared_ptr<TestClass> mTest;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With