24

std::make_shared vs. the Normal std::shared_ptr Constructor

 5 years ago
source link: https://www.tuicool.com/articles/hit/ZnyyaeU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

There are two different ways to create a std::shared_ptr : via one of its constructors and via std::make_shared . Both have their merits and different tradeoffs.

First of all I’d like to thank my colleague Stefan Asbeck for a chat session where we brainstormed about the different aspects I’ll go into. Stefan is a software engineer at the Zühlke office in Munich.

shared_ptr and weak_ptr: a short overview

Let’s quickly recap how std::shared_ptr works: The underlying feature of shared_ptr is a reference count. When we copy a shared_ptr , the count increases. When a shred_ptr gets destroyed, the count decreases. When the count reaches zero, there are no more shared_ptr s to the object and the object gets destroyed.

std::weak_ptr is the companion of shared_ptr : it does not own the object, so it does not contribute to the reference count. It does not contain a pointer to the object itself, because that may become invalid after the object has been destroyed. Instead, there is another pointer to the object alongside the reference count.

weak_ptr refers to the reference count structure and can be converted to a shared_ptr if the count is not zero, i.e. the object still exists. For reasons we’ll see in a second, there has to be another counter for the number of weak_ptr s.

shared_ptr is non-intrusive, which means the count is not stored inside the object itself. This, in turn, means that the count has to be stored somewhere else, on the heap. When a shared_ptr is constructed from an existing pointer that is not another shared_ptr , the memory for the count structure has to be allocated.

The structure has to live as long as there are any shared_ptr s or weak_ptr s left, which may well be after the object has been destroyed. Therefore, the number of weak_ptr s needs to be counted as well.

Conceptually, we can think of the situation like this (the actual implementation details can differ):

vqEBjmu.png!web

std::make_shared

With the above picture, when we create an object managed by shared_ptr , the naive approach takes two memory allocations:

auto* ptr = new MyObject{/*args*/};   //allocates memory for MyObject
std::shared_ptr<MyObject> shptr{ptr}; //allocates memory for the ref count structure

The situation is the same whether we create the shared_ptr from a raw pointer, from a unique_ptr , or by creating an empty shared_ptr and later resetting it with a raw pointer.

As you may know, memory allocations and deallocations are amongst the slowest single operations. For that reason, there’s a way to optimize this into one single allocation:

auto shptr = std::make_shared<MyObject>(/*args*/);

std::make_shared allocates the memory for the reference count structure and the object itself in one block. The object is then constructed by perfectly forwarding the arguments to its constructor.

Pros and cons of make_shard vs. normal shared_ptr construction

As always in life, nothing comes for free. Using make_shared entails some tradeoffs we should be aware of.

Pro make_shared

The big advantage of make_shared is, of course, the reduced number of separate allocations . When the other tradeoffs are not an issue, this is the single reason why we should use make_shared as a default.

Another advantage is cache locality : With make_shared , the count structure and the object are located right beside each other. Actions that work with both the count structure and the object itself will have only half the number of cache misses. That being said, when cache misses are an issue, we might want to avoid working with single object pointers altogether.

Order of execution and exception safetyis another issue that has to be kept in mind, at least before C++17. Imagine this piece of code:

struct A {
  int i;
};

void foo(std::shared_ptr<A>, double d);
double bar_might_throw();

int main() {
  foo(std::shared_ptr<A>(new A{22}),
      bar_might_throw());
}

There are three things that have to be done before foo can be called: constructing and allocating the A , constructing the shared_ptr , and calling bar_might_throw . C++17 introduced more restrictive rules for the evaluation order of function parameters. Before that, that sequence could have looked like this:

new A
bar_might_throw()
shared_ptr<A>

If step 2 throws, step 3 is never reached, no smart pointer takes ownership of the A pointer, and we have a memory leak. make_shared takes care of that issue.

Contra make_shared

One of the regularly encountered drawbacks with make_shared is that it needs access to the constructor it has to call. Making make_shared a friend of our class is not guaranteed to work – the actual constructor call may be done inside a helper function. One possible workaround to this problem is thepasskey idiom. This is a bit clumsy and might not be worth the effort if a second allocation is not an issue.

Another problem might be the lifetime of the object storage (not the object itself). While the pointee object is destroyed when the last shared_ptr releases its ownership, the ref count structure needs to live on until the last weak_ptr is gone. When we use make_shared this includes the storage for the pointee object. When we deal with large objects and long-lived weak_ptr s, that can mean that a considerable amount of empty memory is needlessly locked.

Conclusion

While std::make_shared is a good default for creating shared_ptr s, we have to be aware of the implications. Every best practice has its exceptions, there are no absolute rules.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK