Understanding the inner workings of C++ smart pointers - The shared_ptr
After last months Understanding the inner workings of C++ smart pointers - The unique_ptr with custom deleter you're curious about how the shared_ptr
is implemented? Great! Here we go.
A minimalistic shared_ptr
implementation
Well, minimalistic is a simple word. A shared_ptr
is a little more complex than a unique_ptr
. Below is the implementation of a shared_ptr
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
|
A shared_ptr
needs to track the use count. This tracking is done via a control block. Since multiple shared_ptr
that point to the same data must use the same control block, the shared_ptr
implementation stores only a pointer to that control block, next to the data pointer.
When you create a new shared_ptr
by passing an already allocated pointer to the constructor, the first thing the shared_ptr
does is allocate a new control block where it also stores the data pointer. If you look closely, you can see that in my implementation, shared_ptr
has a data member of type ctrl_blk_base
. At the same time, a ctrl_blk
is allocated in the constructor. I will show you that implementation later. For now, it is safe to assume that inheritance is used.
In the destructor, release_shared
is called on the control block if the latter isn't a nullptr
.
The next thing are the copy and move operations. They use one clever trick. They create a new temporary shared_ptr
object and call swap
. That's an easy and robust way to maintain the use count.
The shared_ptr
is special due to its control block. You can see this in the implementation of make_shared
as well. I'm returning a dynamically allocated object of type ctrl_blk_with_storage
, which triggers another shared_ptr
constructor. Hopefully, obviously, ctrl_blk_with_storage
is also derived from ctrl_blk_base
.
Peaking into shared_ptr
s control block implementation
Below, you will find the control block base class ctrl_blk_base
and the two derived classes ctrl_blk
and ctrl_blk_with_storage
you've seen earlier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
The base class ctrl_blk_base
contains an atomic unsigned long data member and the three member functions add_shared
, dec
, and release_shared
. The first two are helpers to maintain the use count. More interesting is release_shared
since it is virtual
, pure virtual
to be precise. The virtuality here is necessary because the two derived classes have different implementations.
Let's start by looking at ctrl_blk
. This class has a pointer data member that contains the original data. If the use count reaches zero in release_shared
, the function first deletes the payload data before deleting itself. The last step is necessary because the shared_ptr
destructor cannot do this.
If you now switch to ctrl_blk_with_storage
, you can see that this class brings its own storage space for the T
it is constructed with. This is what make_shared
uses. In release_shared
, it is enough to perform a self-delete. This also invokes the destructor of T
.
You just saw a minimalistic example of a shared_ptr
. I ignored the weak_ptr
and its implications. However, I hope this helps you understand the two smart pointers better.
More about smart pointers
I will continue writing about smart pointers in my next post.
Andreas