Why C++ std::enable_shared_from_this sucks in multithreaded environment

This article is meant for people who have experience with C++11's (or boost's) smart pointers. It requires knowledge of at least std::shared_ptr and std::enable_shared_from_this

Technology
  • C++
Level Upper-intermediate
Time to read ~10 minutes

Disclamair

Before starting this rant, I need to make a big statement: I love smart pointers, I love control and ownership pattern they provide and I love countless of memory bugs I don’t need to debug, because they are not there anymore.

The life cycle of a smart pointer

(This is not meant to be complete explanation of smart pointer!)

First of all, before I get into the problem itself, I will cover life cycle of std::shared_ptr's managed object

  1. Allocation ("Library, give me some free memory!")
  2. Initialization ("Constructor, fill some data for this object!")
  3. Assign to std::shared_ptr ("Create new object for storing references count!")
  4. Normal usage, usage, usage...
  5. When reference count drop to zero:
  6. Uninitialization ("Destructor, clean up resources!")
  7. Deallocation ("Library, you can reclaim your memory!")
  8. Destroy std::shared_ptr itself

Note two very important points: 2 and 3. Shared references counter is allocated once per all std::shared_ptr instances for given object. Because of that, following construction is illegal:

std::shared_ptr a1 = std::shared_ptr(new int);
int * raw = a1.get();
std::shared_ptr a2 = std::shared_ptr(raw);

We now have two shared references counter, because the only information a2’s constructor is obtaining is plain raw address pointer. It has no way of determining that this pointer was already managed by other std::shared_ptr.

So, basically when a1 references dies the int will be deallocated, and when a2 dies, the int will be deallocated again, resulting in segmentation fault.

Problem: pass self to function expecting shared_ptr

Shared pointers can be freely copied, so if you already have std::shared_ptr and want to use it as function argument, you just need to pass copy of it. It will still use the same reference counter, just raising references amount by one. But there is a catch: if you want to pass self (this pointer) from given object to a function expecting std::shared_ptr, you do not actually possess std::shared_ptr instance. Remember: this is a raw pointer, so if you upcast this to std::shared_ptr you will end up with the problem from previous paragraph (two shared references counters and crash on second deallocation).

It is where std::enable_shared_from_this comes by.

It is an utility class that allows you to construct std::shared_ptr from this, provided there is at least one std::shared_ptr already created for given object. Because of that (if you remember previous paragraph) you cannot use it in constructor (remember, constructor get called in 2nd step and std::shared_ptr for given object is allocated in 3rd step)

Now the problem itself!

Perceptive reader should have recognized by now that steps 2 and 3 are not actually atomic operations together. In concurrent environment you may have finished construction of given class, register its instance, call it’s method from other thread (for example the one that was registered) and requested shared_from_this, while the std::shared_ptr object is still not yet constructed!

This will result in segmentation fault that is difficult to debug and nightmare to solve. To sum up:

  1. Constructor was called (and completed!)
  2. shared_from_this was requested
  3. std::shared_ptr was constructed with pointer of this object

Foobar, segmentation fault is thrown.

So, how to solve it?

Reorganize code, so you don’t need it. Seriously, I spent some time with mutexes for each allocation of std::shared_ptr, and each method that can be called from different thread. It is pain-in-the-ass and introduces pretty impactful overhead on execution time. And it is still problematic to maintain (you can easily forgot in future revision of your code). There is no easy way to solve it by itself.