Contents
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.
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 shared_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):
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:
As always in life, nothing comes for free. Using make_shared
entails some tradeoffs we should be aware of.
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 safety is 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
- call
bar_might_throw()
- construct
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.
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 the passkey 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.
Permalink
I cannot see why it is necessary for a shared_ptr object to contain a pointer to the objects it refers to since the ref count structure already has a pointer to the same object.
Permalink
Permalink
Permalink
What about creating a shared poiter from weak_ptr using Lock() function ?
Permalink
When lock succeeds, it means the pointee object still lives, as well as the ref count object. There’s no memory allocation involved – it’s little more than copying two pointers.
Permalink
There is a little known problem with make_shared: It will suppress warnings. The first of the following two lines will compile with no warning or error, while the second will fail as expected:
auto zero1 = std::make_shared<uint8_t>(256);
std::shared_ptr<uint8_t> zero2{new uint8_t{256}};
Here is the code in Compiler Explorer:
https://godbolt.org/z/rqbg1a
The problem is that the narrowing happens inside the STL, where all warnings are disabled. This can be a problem for security critical code, and one could argue that make_shared and its siblings should be prohibited there.
Permalink
I agree that this is can be a security issue. Warnings being disabled in the library implementation is implementation specific, though, meaning it might differ between compilers/library implementations.
I think I vaguely remember a defect report stating that the standard should require brace initialization for the implementation of make_shared. I might be wrong though