A tale of two allocations : make_shared and shared_ptr

Pranay Kumar
pranayaggarwal25
Published in
4 min readOct 12, 2019

make_shared vs shared_ptr

make_shared vs shared_ptr

Quick Recap:

Just a quick recap, shared pointers work on the concept of ref count, they maintain a separate control block that stores these count.

The way shared_ptr works is that they maintain -

strong reference count (S) — number of shared_ptr(s) keeping the object alive. The shared object is destroyed (and possibly deallocated) when the last strong ref goes away.

weak reference count (W) — number of active weak_ptr(s) currently observing the object + (S!=0)

Since C++17, A default-constructed weak_this goes along enable_shared_from_this
Strong and weak count

Strong and Weak count(s) are typically incremented using an equivalent of atomic::fetch_add with memory_order_relaxed.

Decrementing requires stronger ordering to ensure safe destruction.

Logical model for shared_ptr constructor

If a shared_ptr is constructed from an existing pointer that is not shared_ptr the memory for the control structure has to be allocated.

Approximate Memory Lyaout

This Control block is destroyed and deallocated when the last weak ref goes away. A shared_ptr construction approach takes two steps

2 Step memory allocation approach

Logical model for object construction using make_shared

make_shared (or allocate_shared) Allocates the memory for the control structure and the object itself in one single mem block.

Approximate Memory Layout
Approximate Memory Lyaout

The object is then constructed by perfectly forwarding the arguments to its constructor.

Single step memory allocation apporach

Pros make_shared over shared_ptr

Performance: Reduced number of separate allocations

Cache locality: Actions that work with both the count structure and the object itself, will have only half the number of cache misses. (In case cache misses are big issue, we might want to avoid working with single object pointers altogether)

Order of execution and exception safety: (concern pre-C++17)

Sample Code

A possible execution ordering is

1) new Lhs(“foo”))
2) new Rhs(“bar”))
3) std::shared_ptr<Lhs>
4) std::shared_ptr<Rhs>

And one important advantage, especially in cases of preC++17 codes, is the execution safety. So look at this snippet. Foo has this function signature, you make the call like this..now before c++17 there is no restriction of argument resolution so one of the possible resolution may look like this — now what is second step throws..you have a leak, right?

Fix 1: Use make_shared

Use make_shared

Fix 2: Code expansion

Code expansion

Pro shared_ptr vs. make_shared

Access to the constructor - make_shared needs access to the constructor it has to call

Lifetime of the object storage (not the object itself) — The second advantage is about the lifetime of the object storage (not the object) this is about the destruction vs deallocation, when the last weak count goes off, then only deallocation would take place. In case of make_shared the single block becomes the bottleneck. For large size objects in association with some long life weak_ptr, this may become problematic.

Approximate Memory Layout

With shared_ptr, You can also specify a custom deleter, if needed!

Conclusion : Unless good reason, follow this

As a guideline usually, make_shared is more preferable to shared_ptr, but there are cases as stated above where one might need shared_ptr as well.

--

--