Reputation: 3181
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.
Upvotes: 1
Views: 6797
Reputation: 238461
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.
No. If you destroy an object of type A
, that calls the destructor of A
, so that is called first.
However, call to B
's destructor will finish first: The destructor of A
begins by executing the destructor body, and then proceeds to destroy the sub objects. The destructor body finishes first, then the sub object destructors, and finally the destructor of A
will be complete.
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?
In the destructor body of A
, make the pointed B
point somewhere else other than the object that is being destroyed:
~A() {
b->a = nullptr;
}
If you point it to null such as in my shown example, then you must also make sure that B
can handle the situation that B::a
may be null i.e. check before accessing through the pointer.
it seems that A's destructor finished without waiting B to destruct.
That's not what we observe. The body of the destructor of A
has finished, but the destructor does not finish until the member destructors finish first.
Upvotes: 1
Reputation: 11321
Just wanted to point out that your comment is incorrect:
~A() {
std::cout << "A destruct done" << std::endl;
}
A destructor is DONE when you get out of the curly braces. You can see that in a debugger, doing step-by-step. That is where b
will be deleted.
Upvotes: 1
Reputation: 119562
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
.
Upvotes: 5
Reputation: 1049
B has a pointer to A, but doesn't deallocate the memory of it (e.g. no delete). So The pointer is removed but not the memory allocated, which is all fine.
Basically, the pointer is on the stack and it contains the address of some (assumed) allocated memory on the heap. Yes it get removed from the stack, but the allocated memory remains. That's what delete
is for. To remove the allocated memory on the heap. However in your case, you don't want that memory to be removed and your pointer is what we call a non-owning pointer. It points to something, but it's not responsible about the cleanup (actually B doesn't own the memory that the pointer is pointing to).
Upvotes: 1