8

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?

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
Filipp
  • 1,843
  • 12
  • 26
  • 1
    Why you feel the order of destruction matters? The destructor of `std::enable_shared_from_this` doesn't do anything much. Your example looks OK to me (assuming you aren't trying to do anything clever in `~A` and `~B`, like down-casting `this` to `C*`) – Igor Tandetnik Mar 15 '20 at 16:52
  • `enable_shared_from_this` is sometimes implemented by having a `weak_ptr` member. Destroying that member would set `weak_count` to 0, freeing the storage. When `enable_shared_from_this` is the first base, this is fine. What about when it isn't – Filipp Mar 15 '20 at 17:00
  • 1
    @S.M. This is not an access problem. I know `enable_shared_from_this` must be an accessible unambiguous base. In my example, it is. `C` is a struct. It inherits publicly. – Filipp Mar 15 '20 at 17:02
  • 1
    Yes, but base access is determined by the thing doing the inheriting, not the thing being inherited from. I can change my example if you want. The actual code on which it is based uses `class` and `public`. I chose `struct` for the example to avoid typing. – Filipp Mar 15 '20 at 17:16
  • 4
    Thus spake The Standard: "**[util.smartptr.weak.dest]** `~weak_ptr();` *Effects:* Destroys this `weak_ptr` object but **has no effect on the object** its stored pointer points to." Emphasis mine. – Igor Tandetnik Mar 15 '20 at 17:30
  • Yes, but here are a few more things I believe to be true. `make_shared` puts the control block and object storage in the same allocation. The control block survives until the last `shared_ptr` or `weak_ptr`. Doesn't this mean the storage is freed when the last `weak_ptr` is destroyed? – Filipp Mar 15 '20 at 19:04
  • 1
    @Filipp The lifetime of the stored object ends when the last `shared_ptr` dies. Even if the `weak_ptr` keeps the control block from being deallocated, I don't think it matters. – HolyBlackCat Mar 15 '20 at 20:30
  • @Filipp I think you are getting confused by irrelevant minutia. Whether `make_shared` is used (and allocates both control info and memory for the object), or whether a `shared_ptr` is constructed from an existing dynamically allocated object, **`weak_ptr` always has shared ownership of that control info.** So either the last `weak_ptr` or the last `shared_ptr` is responsible for freeing that info. Either way, it's internal stuff and makes no diff for you. – curiousguy Mar 18 '20 at 03:13
  • @S.M. Your comments are confusing and incorrect. You should remove them. – curiousguy Mar 18 '20 at 03:33
  • 1
    @HolyBlackCat The question is "_When ~A() and ~B() run,_ (...)" so obviously we know the lifetime of `C` has ended, because the last sharing `shared_ptr` was destroyed (or reset). The Q is about **the handling of resources when there are remaining `weak_ptr` at the point `T` object destruction starts.** – curiousguy Mar 18 '20 at 03:43

3 Answers3

5

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.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • Does not answer my question. Nowhere do I mention attempting to "reconstruct" any C. Answer should be one of "`enable_shared_from_this` can appear anywhere in the base list, implementations are required to free memory after destruction of the whole object, no matter how it inherits from `enable_shared_from_this`", or "It must be the first base, inheriting anywhere else is UB", or "This behavior is unspecified, or quality of implementation". – Filipp Mar 16 '20 at 12:18
  • @Filipp: The answer is a combination -- they can appear anywhere and regardless, an implementation is free to free memory for part of an object after the destruction of that part of the object (and before the destruction of base classes). There is simply no requirement that memory can only be freed after the destruction of the entire object, regardless. – Chris Dodd Mar 16 '20 at 19:33
2

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.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • This is the answer I was looking for! Thank you! The key is that the object being alive actually counts as one implicit weak use. That is `weak_count` is 1 for an object that was `make_shared`-ed even if there are no `weak_ptr`s. Releasing the `shared_ptr` first decrements `use_count` only. If it became 0, the object (but not the control block) is destroyed. **Then** `weak_count` is decremented, and if 0 the control block is destroyed + freed. An object that inherits from `enable_shared_from_this` starts with `weak_count` = 2. A brilliant solution by STL implementers, as expected. – Filipp Mar 18 '20 at 09:43
  • Just a pedantic nitpick: STL is Standard Template Library, which except for historical artefacts (the HP STL, or the SGI STL) is only informally defined; it is about types following the requirements of Containers, Iterators, and the "algorithms" working on those. The STL is not strictly limited to templates, as it uses a few non template classes (f.ex. `random_access_iterator_tag`). There is informal agreement to call anything related with containers part of the STL. tl;dr: Not all templates in the std lib are part of the STL and not all non templates are outside of it. – curiousguy Mar 21 '20 at 14:59
-1

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?

Andreas_75
  • 44
  • 5
  • What you wrote is true, but does not answer my question. Of course base class destructors run after the derived class. I'm asking whether implementations of `shared_ptr`, `weak_ptr`, and `enable_shared_from_this` are required to keep the memory around long enough to make this safe, even when `enable_shared_from_this` isn't the first base. – Filipp Mar 16 '20 at 12:12
  • Ah ok. Looking at your original question: there is no clear 'this' [as in "make this save" in your above comment] you are trying to achieve. Will edit my answer to reflect the question as I understand it at this moment. – Andreas_75 Mar 16 '20 at 13:48
  • Make this safe = inheriting from `enable_shared_from_this` after another base class. – Filipp Mar 16 '20 at 17:42