August 15th, 2023

Inside STL: The shared_ptr constructor vs make_shared

There are two ways to create a new object that is controlled by a shared_ptr.

// From a raw pointer
auto p = std::shared_ptr<S>(new S());

// Via make_shared
auto p = std::make_shared<S>();

They result in two different memory layouts.

In the first case, you manually created a new S object, and then passed a pointer to it to the shared_ptr constructor. The shared_ptr adopts the raw pointer and creates a control block to monitor its lifetime. When the last shared pointer destructs, the Dispose() method deletes the pointer you passed in.¹ When the last shared or weak pointer destructs, the Delete() method deletes the control block.

p    
object
S
freed separately
control
control_block

In the second case, you let the make_shared function create the S object, and in practice, what it does is create a single memory allocation that consists of a control block stacked on top of an S object. This time, when the last shared pointer destructs, the Dispose() method runs the S destructor, but the memory isn’t freed yet. Only when the last shared or weak pointer destructs does the Delete() method get called to free the entire memory block.

p      
object   control_block freed as a unit
control S
 

The two memory layouts have their own pros and cons.

  Two allocations Single allocation
Last shared
pointer destructs
Object destructs
Object memory freed
Object destructs
Object memory not freed
Last shared or weak
pointer destructs
Control block destructs
Control block freed
Control block destructs
Combo block freed
Locality Worse Better
Straggler weak pointer Control block lingers
(Object memory freed already)
Entire combo block lingers

The single-allocation version has better memory locality since the control block is kept right next to the managed object.

On the other hand, with the single-allocation version, a straggler weak pointer (a weak pointer which lives for a long time after the last shared pointer has destructed) prevents the entire combo memory block from being freed. By comparison, only the control block remains in memory with the two-allocation version.

If your weak pointers exist to break circular references, then you won’t have stragglers because they will go away when the object graph destructs. Similarly, if your weak pointers are in event handlers, then those weak pointers won’t be stragglers if you are careful to unregister the event handlers at destruction. The stragglers come into play if you retain weak references in, say, a cache or other long-lifetime storage, and even then, they cause a problem only if sizeof(S) is significant or if you have a lot of them.

Next time, we’ll look at make_shared‘s close friend, std::enable_shared_from_this.

¹ More specifically, the Deleter object deletes the pointer you passed in. The default deleter uses the delete operator to delete the pointer.

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

7 comments

Discussion is closed. Login to edit/delete existing comments.

  • Alessandro Morelli · Edited

    For common use cases this is typically less of a concern, but there are other performance implications to using std::make_shared. By allocating the control block in memory right before the object S, members of S might get “pushed” to another cache line. This might introduce a measurable performance drawback in the application.

  • Chris Leonard

    So std::make_shared is really only appropriate with small classes and classes that point to their content, like std::vector? News to me, I thought it was good practice to use it everywhere!

    • Raymond ChenMicrosoft employee Author

      My third-to-last paragraph says the opposite! If you like the improved locality, and your weak pointers don’t outlive your strong pointers for long enough that the extra memory usage is concerning, then go ahead and reap the locality gains of the combined allocation.

      • Chris Leonard

        "[if] your weak pointers don’t outlive your strong pointers for long enough that the extra memory usage is concerning"

        But if I wasn't concerned, and I'm using a fat class, I wouldn't be using weak_ptr ... except in situations where it's necessary to break an ownership cycle in a graph as you said. That's the only situation I can think of where I'm not concerned. So appropriate with small classes... or when using weak_ptr to break...

        Read more
      • Raymond ChenMicrosoft employee Author

        There are other scenarios beyond “breaking cycles” where you have weak pointers that will not outlive the main object by long, so “except for cycle-breaking” may be too strict.

      • Yuri Khan

        Not the first time that the standard library “forces” a not-always-valid optimization on you. Another is std::vector<bool>.

      • Axel RietschinMicrosoft employee

        It’s not only locality, but less overhead (heap memory block header of some sort for each allocation) and ultimately less heap fragmentation.