Modern (Effective) C++ - std::shared_ptr

Here we discuss about workings, performance and characteristics of std::shared_ptr and end the article with a test program which compares the performance of std::shared_ptr vs std::unique_ptr.

This article is inspired by Item 19 of Effective Modern C++ by Scott Meyers.

Just like the std::unique_ptr , std::shared_ptr is used to manage the lifetime of an underlying raw pointer, and delete it when it's no longer needed. However shared_ptr (going to drop the std prefix from now on) can be copied to a new shared_ptr ( sp_new = sp_orig) or copy constructed ( shared_ptr sp2 = sp1 , thus resulting in two or more shared_ptrs pointing to the same raw pointer. These shared_ptrs now share the ownership of the underlying pointer. They co-operate with each other ( by using reference count ) to decide when to finally delete the raw pointer.

These are some important features of std::shared_ptr -
  • They are twice the size of raw pointer (or unique_ptr), because we need another pointer pointing to the control block which consists of the reference count object.
  • Increments and decrements on the reference count should be atomic, since shared_ptrs can be spread out in different threads. If two threads decrement the reference count, they should be ordered using atomic variables or mutex ( one after the other), otherwise we might lose one decrement. 

Shared_ptr supports copy assignment, copy construction, move assignment, move construction. Moving the shared_ptr to a new shared_ptr, works like move on any other variable type.
For instance -
shared_ptr sp2 = std::move(sp1);
Here the state of sp1 is transferred to sp2, along with raw pointer, reference count, deleters etc ( collectively known as control block). and sp1 is set to null. Also note that in move construction, reference count is unchanged.

Just like std::unique_ptr, std::shared_ptr also supports custom deleters. However the difference is, deleters is not a part of the shared_ptr itself.
For example -

This enables different shared_ptrs with different deleters to be assigned (copy or move) to each other, and to co exist in a single collection like vector. We can also notice that deleter function with captured variables does not increase the size of the shared_ptr. This is because the captured variables is not part of shared_ptr, but is allocated in the free space within the control block.

Here are some rules for creation of control block -
  • Creating shared_ptr using make_shared always creates a control block.
  • It is created when a shared_ptr is constructed from unique_ptr. ( unique_ptr is then set to null).
  • When shared_ptr is created using raw pointer.

It should also be noted that we cannot use make_shared when supplying custom deleter.
De-referencing a shared_ptr costs the same as de-referencing a raw pointer.

If we want to create shared_ptr from this object, then our class must inherit from enable_shared_from_this<OurClass>.
To create shared_ptr of this (current object), from any of the member function, we should call shared_from_this() . Keep in mind that before calling shared_from_this(), shared_ptr for this object must already be present. Hence to enforce this, such classes make constructors private, and have a factory kind of method which returns shared_ptr.

We can create a shared_ptr from unique_ptr, but not vice versa. We cannot create (convert) unique_ptr from shared_ptr even when reference count is 1.

Here is a program to compare the performance of unique_ptr VS shared_ptr


PS : Sean Parent on the C++ seasoning talk (Youtube) discourages using shared_ptr as it removes our ability to reason about the code locally, and makes the usage of shared_ptr almost similar to global variables.

Modern (Effective) C++ - std::shared_ptr Modern (Effective) C++ - std::shared_ptr Reviewed by zeroingTheDot on February 03, 2018 Rating: 5

No comments:

Powered by Blogger.