Difference in make_shared and normal shared_ptr in C++
Asked Answered
E

8

401
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

I am not able to understand why make_shared is more efficient than directly using shared_ptr.

I need a step by step explanation of the sequence of objects created and operations done by both, so that I understand how make_shared is efficient. I have given one example above for reference.

Erg answered 3/1, 2014 at 2:44 Comment(7)
It isn't more efficient. The reason to use it is for exception safety.Hazelton
STL covered this in one of his videos on Channel 9. It was probably this one.Narwhal
@Yuushi: Exception safety is a good reason to use it, but it's also more efficient.Linage
32:15 is where he starts in the video I linked to above, if that helps.Narwhal
Minor code style advantage: using make_shared you can write auto p1(std::make_shared<A>()) and p1 will have the correct type.Ufo
Could someone please explain the exception safety in this case?Mesotron
@MawgsaysreinstateMonica: That's already explained in the top answer. It's apparently not an issue in C++17, but in C++11 and C++14, creating two shared_ptrs in a single function call might run both news before wrapping either; if the second fails, the first one is leaked.Zhdanov
G
469

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.

Gildea answered 3/1, 2014 at 2:51 Comment(24)
It's a good idea to mention the small corner-case downside of make_shared as well: since 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.Feinberg
@Gildea so if i call reset() on the shared_ptr then even the control block is reset, in case of make_shared? is this also the case in direct creation?Blindstory
@Koushik Calling reset() will decrement the shared_ptr count in the control block in both cases. Is that what you meant by "control block is reset"?Gildea
Another, more stylistic, point is: If you use make_shared and make_unique consistently, you won't have owning raw pointers an can treat every occurrence of new as a code smell.Fig
@Gildea no what I meant was, if there is only one owner to the pointer then will the control block be deleted when I do reset(), when using make_shared()?.Blindstory
If there is only one shared_ptr, and no weak_ptrs, calling reset() on the shared_ptr instance will delete the control block. But this is regardless or whether make_shared was used. Using make_shared makes a difference because it could prolong the life-time of the memory allocated for the managed object. When the shared_ptr count hits 0, the destructor for the managed object gets called regardless of make_shared, but freeing its memory can only be done if make_shared was not used. Hope this makes it more clear.Gildea
Forgot to tag you, @KoushikGildea
It would also be worth mentioning that make_shared can take advantage of the "We Know Where You Live" optimization that permits the control block to be a pointer smaller. (For details, see Stephan T. Lavavej's GN2012 presentation at about minute 12.) make_shared thus not only avoids an allocation, it also allocates less total memory.Indusium
How can you see the fact that both "std::make_shared performs a single heap-allocation accounting for the space necessary for both the control block and the data"? How can this be done? Could someone please guide me?Spurtle
@HannaKhalil you should be able to write a simple program and run valgrind to observe the number of allocations.Gildea
@Gildea I was asking implementation wise. I can't think how one can programmatically unify these 2 allocations to a single allocation and looking at the implementation is not helpingSpurtle
@HannaKhalil: Is this perhaps the realm of what you're looking for...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGHGildea
@Fig 1) Doesn't that exclude using reset() 2) How do you assign a pointer to an array (no std::array) to std::unique_ptr without using new?Hoyden
So is the upshot of this that using make_shared undermines the concept of weak_ptrs to some degree? Inasmuch as the memory will essentially leak if a weak_ptr is hanging around, even if the main target object has been destructed by the removal of the final shared_ptr.Doura
@mpark, this is an interesting point. I wonder if the shared_ptr created by make_share would purge both the control block and the pointee, and then create a new control block that is reset. Seems this is probably generally preferred since we would expect that the most common scenario would have sizeof(pointee) >> sizeof(control block)Controller
@Feinberg Do weak_ptr has its own control block, or it simply refer to share_ptr's control block? Your comment seems to indicate the latter, just want to confirm. :DWallacewallach
@Wallacewallach The control block corresponds to an allocation. All shared_ptrs and weak_ptrs that denote that same allocation share the same control block. Which incidentally makes owner_less work.Feinberg
I think that your "std::shared_ptr manages two entities: ...." is a better explanation of the ownership and memory than the explanation at cppreference.Westcott
This Update I: Exception-Safety part is invalid. Compiler can't partially evaluate arguments of F. If argument is evaluated then it must be complete evaluation. So shown order of evaluation is impossible and this example and explanation is invalid. AFAIK current implementation of shared_ptr do not have problem with exception safety when instantiate shared_ptr (some old version had this issue when allocating object was successful, but allocating control block failed).Shotwell
Thanks @MarekR, I've updated the answer to point out that the exception-safety aspect of it is indeed obsolete since C++17. Did I understand you correctly?Gildea
@HannaKhalil char * p = malloc(sizeof(T) + sizeof(control)); T * t = new(p) T(args...); control * c = new(p + sizeof(T)) control(1, 0);. i.e. placement-newRecede
I think single heap allocation(make_shared) is safer from weak_ptr perspective comparing to calling shared_ptr constructor directly using new. Using new leaves a small (or large?) window where weak_ptr will hold the lock on control block while the actual data block on heap has been freed.Tepic
@Gildea does the control block contain meta data that indicates if it corresponds to "1" or "2" allocations, or is this typically inferred from difference in addresses of the object relative to control block?Australorp
Maybe another benefit of make_shared is that since the control block and pointee object are adjacent, there will be almost half cache misses compared with constructor approach in which the two parts are allocated separately.Sacramentalism
H
33

There is another case where the two possibilities differ, on top of those already mentioned: if you need to call a non-public constructor (protected or private), make_shared might not be able to access it, while the variant with the new works fine.

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};
Hug answered 25/10, 2016 at 7:51 Comment(3)
I ran into this exact problem and decided to use new, otherwise I would have used make_shared. Here is a related question about that: #8147527.Keramics
could this be overcome using friend?Oday
@Oday , I doubt it is possible, since the object may be created by the functions called from make_shared, and they are implementation-specific.Benis
L
29

The shared pointer manages both the object itself, and a small object containing the reference count and other housekeeping data. make_shared can allocate a single block of memory to hold both of these; constructing a shared pointer from a pointer to an already-allocated object will need to allocate a second block to store the reference count.

As well as this efficiency, using make_shared means that you don't need to deal with new and raw pointers at all, giving better exception safety - there is no possibility of throwing an exception after allocating the object but before assigning it to the smart pointer.

Linage answered 3/1, 2014 at 2:52 Comment(1)
I understood your first point correctly. Can you please elaborate or give some links on the second point about exception safety?Erg
B
11

I see one problem with std::make_shared, it doesn't support private/protected constructors

std::shared_ptr(new T(args...)) may call a non-public constructor of T if executed in context where it is accessible, while std::make_shared requires public access to the selected constructor.

https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared#Notes

Boldfaced answered 30/8, 2018 at 16:7 Comment(0)
R
6

If you need special memory alignment on the object controlled by shared_ptr, you cannot rely on make_shared, but I think it's the only one good reason about not using it.

Rompish answered 10/1, 2014 at 22:36 Comment(1)
A second situation where make_shared is inappropriate is when you want to specify a custom deleter.Indusium
C
4

Shared_ptr: Performs two heap allocation

  1. Control block(reference count)
  2. Object being managed

Make_shared: Performs only one heap allocation

  1. Control block and object data.
Cubicle answered 17/9, 2017 at 11:51 Comment(0)
C
1

I think the exception safety part of mr mpark's answer is still a valid concern. when creating a shared_ptr like this: shared_ptr< T >(new T), the new T may succeed, while the shared_ptr's allocation of control block may fail. in this scenario, the newly allocated T will leak, since the shared_ptr has no way of knowing that it was created in-place and it is safe to delete it. or am I missing something? I don't think the stricter rules on function parameter evaluation help in any way here...

Choosey answered 13/10, 2019 at 13:38 Comment(1)
The standard guarantees that the object will be deleted in this situation eel.is/c++draft/util.smartptr.shared.const#6Cathee
S
0

About efficiency and concernig time spent on allocation, I made this simple test below, I created many instances through these two ways (one at a time):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

The thing is, using make_shared took the double time compared with using new. So, using new there are two heap allocations instead of one using make_shared. Maybe this is a stupid test but doesn't it show that using make_shared takes more time than using new? Of course, I'm talking about time used only.

Sills answered 10/3, 2017 at 16:31 Comment(2)
That test is somewhat pointless. Was the test done in release configuration with optimisations turned out? Also all of your items are freed immediately so it is not realistic.Seamaid
Debug performance is important to. Optimizations that degrade debugging performance degrade developers experience.Jaban

© 2022 - 2024 — McMap. All rights reserved.