Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Must enable_shared_from_this be the first base class?

My class inherits from multiple bases, one of which is std::enable_shared_from_this. Must it be the first base?

Suppose the following example code:

struct A { ~A(); };
struct B { ~B(); };
struct C : A, B, std::enable_shared_from_this<C> {};

std::make_shared<C>(); 

When ~A() and ~B() run, can I be sure that the storage where C lived is still present?

like image 457
Filipp Avatar asked Mar 15 '20 16:03

Filipp


People also ask

How does Enable_shared_from_this work?

std::enable_shared_from_this is a standard solution that enables a shared_ptr managed object to acquire a shared_ptr to itself on demand. A class T that publicly inherits an std::enable_shared_from_this<T> encapsulates a std::weak_ptr<T> to itself that can be converted to a std::shared_ptr<T> when needed.

What is enable_ shared_ from_ this?

std::enable_shared_from_this allows an object t that is currently managed by a std::shared_ptr named pt to safely generate additional std::shared_ptr instances pt1, pt2, ... that all share ownership of t with pt .


2 Answers

When ~A() and ~B() run, can I be sure that the storage where C lived is still present?

No, and the order of the base classes is irrelevant. Even the use (or not) of enable_shared_from_this is irrelevnt.

When a C object is destroyed (however that happens), ~C() will be called before both ~A() and ~B(), as that is the way that base destructors work. If you try to "reconstruct" the C object in either base destructor and access fields in it, those fields will have already been destroyed, so you will get undefined behavior.

like image 65
Chris Dodd Avatar answered Nov 08 '22 12:11

Chris Dodd


When ~A() and ~B() run, can I be sure that the storage where C lived is still present?

Of course! It would be hard to use a base class that tries to free its own memory (the memory where it resides). I'm not sure it's even formally legal.

Implementations don't do that: when a shared_ptr<T> is destructed or reset, the reference count (RC) for the shared ownership of T is decremented (atomically); if it reached 0 in the decrement, then destruction/deletion of T is started.

Then the weak-owners-or-T-exists count is decremented (atomically), as T no longer exists: we need to know if we are the last entity standing interested in the control block; if the decrement gave a non zero result, it means some weak_ptr exist that share (could be 1 share, or 100%) ownership of control block, and they are now responsible for the deallocation.

Either way, atomic decrement will at some point end up with a zero value, for the last co-owner.

Here there are no threads, no non-determinism, and obviously the last weak_ptr<T> was destroyed during destruction of C. (The unwritten assumption in your question being that no other weak_ptr<T> was kept.)

Destruction always happen in that exact order. The control block is used for destruction, as no shared_ptr<T> knows (in general) which (potentially non virtual) destructor of (potentially different) most derived class to call. (The control block also knows not to deallocate memory on shared count reaching zero for make_shared.)

The only practical variation between implementations seems to be about the fine details of memory fences and avoiding some atomic operations in common cases.

like image 20
curiousguy Avatar answered Nov 08 '22 12:11

curiousguy