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
|Time to read||~10 minutes|
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
- Allocation ("Library, give me some free memory!")
- Initialization ("Constructor, fill some data for this object!")
- Assign to
std::shared_ptr("Create new object for storing references count!")
- Normal usage, usage, usage...
- When reference count drop to zero:
- Uninitialization ("Destructor, clean up resources!")
- Deallocation ("Library, you can reclaim your memory!")
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:
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
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
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
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:
- Constructor was called (and completed!)
- shared_from_this was requested
std::shared_ptrwas 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.