First, you should use std::make_shared (most of the time) to create shared pointers. It is optimized to perform only one allocation for both the reference count / control block and the object it holds.

It is also safer to use std::make_shared in the presence of exceptions:

some_function(std::shared_ptr<T>(new T), std::shared_ptr<T>(new T));

Prior to C++17, one of the possible orders of events could have been:
1) new T
2) new T
3) std::shared_ptr<T>(...)
4) std::shared_ptr<T>(...)

So in the worse case the code could leak an instance of T if the second allocation in step #2 failed. That is not the case with C++17. It has more stringent requirements on the order of function parameter evaluation. C++17 dictates that each parameter must be evaluated completely before the next one is handled. So the new order of events becomes:
1) 1st new T
2) 1st std::shared_ptr<T>(...)
3) 2nd new T
4) 2nd std::shared_ptr<T>(...)
or:
1) 2nd new T
2) 2nd std::shared_ptr<T>(...)
3) 1st new T
4) 1st std::shared_ptr<T>(...)

Shared pointers can also be used with arrays by providing a custom deleter, like this:

auto p = std::shared_ptr<T>(new T[N], [](T* ptr) { delete [] ptr; });

Unfortunately you can not use std::make_shared for that, and it is much easier to use arrays with std::unique_ptr since std::shared_ptr does not provide an overloaded operator [], but about that in my next post 😉

Disadvantages of using std::make_shared:

std::make_shared can only construct instances of T using T’s public constructors. If you need to create an instance using private or protected constructor you have to do it from within a member or static-member function of T. It is not possible to declare std::make_shared to be a friend of T (technically you can declare it to be a friend, but during invocation of std::make_shared it will fail a static_assert inside std::is_constructible type trait class, so for all intents and purposes friendship is not possible here).

std::weak_ptr makes things even more complicated:

Finally, let’s take a look at when instances of objects held by std::shared_ptr are destroyed and their memory released. Under normal circumstances the destructor of T held by a shared pointer will be called when the last shared pointer is destroyed, and the memory where T lived will be released.

That is not the case if the shared pointer of T was constructed using std::make_shared and std::weak_ptr was made to point at it.

In order to function properly std::weak_ptr must hold a reference to the shared pointer’s control block so that it can: 1) answer the call to use_count() and 2) return a nullptr when lock() is called on it after the last shared pointer went out of scope. If the shared pointer’s control block and the instance of T lived in the same memory block, that memory can not be freed until all shared and weak pointers referencing it go away. Now, the destructor of T will be called when the last shared pointer goes away, but the memory will linger until the remaining weak pointers are gone.

I didn’t mention anything about passing shared pointers as function arguments or return values… but that’s a topic about higher level design of object ownership and lifetime; maybe I’ll write about it after I cover unique pointers.

4 Replies to “In-depth look at C++ shared pointers”

  1. Nice Topic. I was just watching a video of CppCon and “Nicolai” talked about the pitfall of std::make_shared.

Leave a Reply