Must enable_shared_from_this be the first base class?

417 Views Asked by At

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?

3

There are 3 best solutions below

2
curiousguy On BEST ANSWER

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.

3
Andreas_75 On

If you create an object c of type C, with bases A, B and a reference counter through inheriting from base enable_shared_from_this<T>, first of all memory is allocated for the whole resulting object, including bases in general and the base enable_shared_from_this<T>. The object will not be destructed until the last owner (a.k.a. the shared_ptr) is relinquishing ownership. At that moment ~enable_shared..., ~B and ~A will be run after ~C. The complete allocated memory is still guaranteed to be there until after the last destructor ~A is run. After ~A is run, the complete object memory is freed in one fell swoop. So to answer your question:

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

Yes it is, although you cannot legally access it, but why would you need to know? Which problem are you trying to avoid?

2
Chris Dodd On

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.