Difference in Make_Shared and Normal Shared_Ptr in C++

Difference in make_shared and normal shared_ptr in C++

The difference is that std::make_shared performs one heap-allocation, whereas calling the std::shared_ptr constructor performs two.

Where do the heap-allocations happen?

std::shared_ptr manages two entities:

  • the control block (stores meta data such as ref-counts, type-erased deleter, etc)
  • the object being managed

std::make_shared performs a single heap-allocation accounting for the space necessary for both the control block and the data. In the other case, new Obj("foo") invokes a heap-allocation for the managed data and the std::shared_ptr constructor performs another one for the control block.

For further information, check out the implementation notes at cppreference.

Update I: Exception-Safety

NOTE (2019/08/30): This is not a problem since C++17, due to the changes in the evaluation order of function arguments. Specifically, each argument to a function is required to fully execute before evaluation of other arguments.

Since the OP seem to be wondering about the exception-safety side of things, I've updated my answer.

Consider this example,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
std::shared_ptr<Rhs>(new Rhs("bar")));

Because C++ allows arbitrary order of evaluation of subexpressions, one possible ordering is:

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

Now, suppose we get an exception thrown at step 2 (e.g., out of memory exception, Rhs constructor threw some exception). We then lose memory allocated at step 1, since nothing will have had a chance to clean it up. The core of the problem here is that the raw pointer didn't get passed to the std::shared_ptr constructor immediately.

One way to fix this is to do them on separate lines so that this arbitary ordering cannot occur.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

The preferred way to solve this of course is to use std::make_shared instead.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Update II: Disadvantage of std::make_shared

Quoting Casey's comments:

Since there there's only one allocation, the pointee's memory cannot be deallocated until the control block is no longer in use. A weak_ptr can keep the control block alive indefinitely.

Why do instances of weak_ptrs keep the control block alive?

There must be a way for weak_ptrs to determine if the managed object is still valid (eg. for lock). They do this by checking the number of shared_ptrs that own the managed object, which is stored in the control block. The result is that the control blocks are alive until the shared_ptr count and the weak_ptr count both hit 0.

Back to std::make_shared

Since std::make_shared makes a single heap-allocation for both the control block and the managed object, there is no way to free the memory for control block and the managed object independently. We must wait until we can free both the control block and the managed object, which happens to be until there are no shared_ptrs or weak_ptrs alive.

Suppose we instead performed two heap-allocations for the control block and the managed object via new and shared_ptr constructor. Then we free the memory for the managed object (maybe earlier) when there are no shared_ptrs alive, and free the memory for the control block (maybe later) when there are no weak_ptrs alive.

When should I prefer `shared_ptr` to `make_shared`?

One of the situations is that std::make_shared does not support the specifying of the custom deleter.

Unlike the std::shared_ptr constructors, std::make_shared does not allow a custom deleter.

You can only do it with the constructor of std::shared_ptr, e.g.

std::shared_ptr<Foo> sh5(new Foo, [](auto p) {
std::cout << "Call delete from lambda...\n";
delete p;
});

Another issue is just as the linked post explained, std::make_shared performs only one allocation for both the control block and the object pointed to. That means after the object being destroyed, the memory it occupied might not be deallocated immediately. That might cause some memory usage issue.

std::shared_ptr initialization: make_sharedFoo() vs shared_ptrT(new Foo)

Both examples are rather more verbose than necessary:

std::shared_ptr<int> p(new int);  // or '=shared_ptr<int>(new int)' if you insist
auto p = std::make_shared<int>(); // or 'std::shared_ptr<int> p' if you insist

What's the difference?

The main difference is that the first requires two memory allocations: one for the managed object (new int), and one for the reference count. make_shared should allocate a single block of memory, and create both in that.

Which one should I prefer and why?

You should usually use make_shared as it's more efficient. As noted in another answer, it also avoids any possibility of a memory leak, since you never have a raw pointer to the managed object.

However, as noted in the comments, it has a potential disadvantage that the memory won't be released when the object is destroyed, if there are still weak pointers preventing the shared count from being deleted.


EDIT 2020/03/06:

Further recommendations come also from the official Microsoft documentation with associated examples. Keep the focus on the Example 1 snippet:

Whenever possible, use the make_shared function to create a shared_ptr
when the memory resource is created for the first time. make_shared is
exception-safe. It uses the same call to allocate the memory for the
control block and the resource, which reduces the construction
overhead. If you don't use make_shared, then you have to use an
explicit new expression to create the object before you pass it to the
shared_ptr constructor. The following example shows various ways to
declare and initialize a shared_ptr together with a new object.

new and make_shared for shared pointers

  1. The code referred to as the second variable is in fact this (taken from OP's code):

    auto ptr_res2(new Object("new"));

    This does not create a std::shared_ptr, it creates a pointer to Object.

  2. When creating a std::shared_ptr using its constructor that takes a naked pointer, you must pass a pointer to already allocated memory (e.g. allocated using new). This means that the memory for the object has already been allocated when creating the std::shared_ptr object itself. The std::shared_ptr needs to allocate memory for its own workings, like e.g. a reference counter. So there are 2 allocations: the one using new passed to the ctor of std::shared_ptr and the one required when constructing the std::shared_ptr itself.

    Object* ptr = new Object{"Foo"}; // Allocation 1 (object).
    std::shared_ptr<Object> p1{ptr}; // Allocation 2 (internal counters).

    The helper function std::make_shared uses only 1 allocation as you pass it the arguments needed to construct the object, not a pointer to the object itself. std::make_shared can then allocate memory once that holds both the object and the ref counter.

    auto p2 = std::make_shared<Object>{"Foo"} // Allocation 1 (object & counter).

Does shared_ptr a = make_shared() create a copy of the shared_ptr before constructor is run?

There are two things to avoid the copy:

  • 1 is the compiler's RVO (return value optimization);
  • 2 is the move constructor/assignment.

for code auto foo = std::make_shared<Foo>();
RVO will create the object directly on the stack. and even we disable the RVO by -fno-elide-constructors, the move constructor will try used as the returned object from make_shared is a temporary one.

Below is a simple test code. (this code only show the concept but not for a real-world shared_ptr implementation)

#include <iostream>

template <typename T>
struct my_shared_ptr
{
T *t_{nullptr};

my_shared_ptr(T *t): t_(t) {
std::cout << "constructor" << std::endl;
};

my_shared_ptr(const my_shared_ptr<T>&) {
std::cout << "copy" << std::endl;
}

my_shared_ptr<T>& operator=(const my_shared_ptr<T>&) {
std::cout << "copy" << std::endl;
return *this;
}

#ifndef NO_MOVE
my_shared_ptr(my_shared_ptr<T>&&) {
std::cout << "move" << std::endl;
}

my_shared_ptr<T>& operator=(my_shared_ptr<T>&&) {
std::cout << "move" << std::endl;
return *this;
}
#endif
};

template <typename T>
my_shared_ptr<T>
my_make_shared() {
return my_shared_ptr<T>(new T);
}

struct Foo {};

int main()
{
auto foo = my_make_shared<Foo>();
return 0;
}

Condition 1, compile with c++11 shows:

$ g++ a.cc -std=c++11 ; ./a.out
constructor

Condition 2, compile with c++11/disable RVO shows:

$ g++ a.cc -std=c++11 -fno-elide-constructors ; ./a.out
constructor
move
move

Condition 3, compile with c++11/disable RVO/no move shows:

$ g++ a.cc -std=c++11 -fno-elide-constructors -DNO_MOVE ; ./a.out
constructor
copy
copy

What happens when using make_shared

The first case does not perform a double allocation, it performs two allocations, one for the managed object and one for the control block of the shared_ptr.

For the second case, cppreference has a good explanation for why std::make_shared usually only performs one memory allocation it says (emphasis mine going forward):

This function typically allocates memory for the T object and for the
shared_ptr's control block with a single memory allocation (it is a
non-binding requirement in the Standard)
. In contrast, the declaration
std::shared_ptr p(new T(Args...)) performs at least two memory
allocations, which may incur unnecessary overhead.

and from std::shared_ptr section it says:

When shared_ptr is created by calling std::make_shared or
std::allocate_shared, the memory for both the control block and the
managed object is created with a single allocation. The managed object
is constructed in-place in a data member of the control block. When
shared_ptr is created via one of the shared_ptr constructors, the
managed object and the control block must be allocated separately. In
this case, the control block stores a pointer to the managed object.

This make_shared description is consistent with the C++11 draft standard which says in section 20.7.2.2.6 shared_ptr creation

template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
template<class T, class A, class... Args>
shared_ptr<T> allocate_shared(const A& a, Args&&... args);

[...]

Remarks: Implementations should perform no more than one memory
allocation. [ Note: This provides efficiency equivalent to an
intrusive smart pointer. —end note ]

[ Note: These functions will typically allocate more memory than
sizeof(T) to allow for internal bookkeeping structures such as the
reference counts. —end note ]

Herb Sutter has a more detailed explanation of the advantages of using make_shared in GotW #89 Solution: Smart Pointers and points out some advantages:

  • It reduces allocation overhead
  • It improves locality.
  • Avoids an explicit new.
  • Avoids an exception safety issue.

Be aware that when using std::weak_ptr using make_shared has some disadvantages.

Is make_shared really more efficient than new?

As infrastructure I was using llvm/clang 3.0 along with the llvm std c++ library within XCode4.

Well that appears to be your problem. The C++11 standard states the following requirements for make_shared<T> (and allocate_shared<T>), in section 20.7.2.2.6:

Requires: The expression ::new (pv) T(std::forward(args)...), where pv has type void* and points to storage suitable to hold an object of type T, shall be well formed. A shall be an allocator (17.6.3.5). The copy constructor and destructor of A shall not throw exceptions.

T is not required to be copy-constructable. Indeed, T isn't even required to be non-placement-new constructable. It is only required to be constructable in-place. This means that the only thing that make_shared<T> can do with T is new it in-place.

So the results you get are not consistent with the standard. LLVM's libc++ is broken in this regard. File a bug report.

For reference, here's what happened when I took your code into VC2010:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor
Destructor

I also ported it to Boost's original shared_ptr and make_shared, and I got the same thing as VC2010.

I'd suggest filing a bug report, as libc++'s behavior is broken.

Are there any downsides with using make_shared to create a shared_ptr

I know of at least two.

  • You must be in control of the allocation. Not a big one really, but some older api's like to return pointers that you must delete.
  • No custom deleter. I don't know why this isn't supported, but it isn't. That means your shared pointers have to use a vanilla deleter.

Pretty weak points. so try to always use make_shared.



Related Topics



Leave a reply



Submit