If class A
contains class B
, then when A
destruct, B
's destructor will be called first, i.e., the reversed order of their nested relationship.
But what if A
contains a shared_ptr
of B
, while B
contains a raw pointer to A
, how should we handle the destructor to make it safe?
Considering the following example:
#include <iostream>
#include <memory>
#include <unistd.h>
struct B;
struct A {
int i = 1;
std::shared_ptr<B> b;
A() : b(std::make_shared<B>(this)) {}
~A() {
b = nullptr;
std::cout << "A destruct done" << std::endl;
}
};
struct B {
A *a;
B(A *aa) : a(aa) {}
~B() {
usleep(2000000);
std::cout << "value in A: " << a->i << std::endl;
std::cout << "B destruct done" << std::endl;
}
};
int main() {
std::cout << "Hello, World!" << std::endl;
{
A a;
}
std::cout << "done\n";
return 0;
}
You can see in A
's destructor, I explicitly set b to nullptr
, which will trigger B
's destructor immediately, and blocking until it finish.
The output will be:
Hello, World!
value in A: 1
B destruct done
A destruct done
done
but if I comment out that line
~A() {
// b = nullptr; // <---
std::cout << "A destruct done" << std::endl;
}
The output will be:
Hello, World!
A destruct done
value in A: 1
B destruct done
done
it seems that A
's destructor finished without waiting B
to destruct. But in this case, I expected segment fault, since when A
already destructed, B
tried to access the member of A
, which is invalid. But why the program doesn't produce segment fault? Does it happen to be OK (i.e., undefined behavior
)?
Also, when I change
{
A a;
}
to
A * a = new A();
delete a;
the output is still the same, no segment fault.
It is important to be precise about what is happening. When A
is destroyed, the following events happen in the following order:
A::~A()
is called.A
object's lifetime ends. The object still exists, but is no longer within its lifetime. ([basic.life]/1.3)A::~A()
is executed.A::~A()
implicitly calls the destructors of direct non-static members of A
in reverse declaration order ([class.dtor]/9, [class.base.init]/13.3)A::~A()
returns.A
object ceases to exist ([class.dtor]/16). The memory that it used to occupy becomes "allocated storage" ([basic.life]/6) until it is deallocated.(All references are to the C++17 standard).
In the second version of the destructor:
~A() {
std::cout << "A destruct done" << std::endl;
}
after the statement is printed, the member b
is destroyed, which causes the owned B
object to be destroyed. At that point, i
has not yet been destroyed, so it is safe to access it. After that, the B
destructor returns. Then, i
is "destroyed" (see CWG 2256 for some subtleties). Finally, the destructor of A
returns. At that point, it would no longer be legal to attempt to access the member i
.