new and make_shared for shared pointers
Asked Answered
M

4

8

I came across this post and one of the answers by @kerek SB states

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

In your code, the second variable is just a naked pointer, not a shared pointer at all.

Now on the meat. make_shared is (in practice) more efficient, because it allocates the reference control block together with the actual object in one single dynamic allocation. By contrast, the constructor for shared_ptr that takes a naked object pointer must allocate another dynamic variable for the reference count. The trade-off is that make_shared (or its cousin allocate_shared) does not allow you to specify a custom deleter, since the allocation is performed by the allocator.

(This does not affect the construction of the object itself. From Object's perspective there is no difference between the two versions. What's more efficient is the shared pointer itself, not the managed object.)

Now I have two questions regarding this post and would appreciate it if someone could clarify this

  1. Why is the second one not a shared pointer ? Will that not increment a reference count

  2. How does make_shared only make one memory allocation and new makes two thus making make_shared more efficent ?

A little clarification on this would be appreciated.

Melodie answered 22/11, 2014 at 21:39 Comment(3)
new Object("foo") is evaluated first, and independently, creating a block which doesn't know it's going to be managed by a shared pointer, and thus doesn't have size for the reference count.Neogene
Hopefully someone pinged KerrekChurrigueresque
See What happens when using make_sharedNikolos
H
4
  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).
    
Hamlett answered 22/11, 2014 at 21:56 Comment(0)
M
12

In that question, the "second variable" referred to this line:

auto ptr_res2(new Object("new")); // this creates an Object*

Not this one:

std::shared_ptr<Object> p2(new Object("foo")); // this creates a shared_ptr<Object>

The best explanation for why make_shared is more efficient with one allocation is to compare images. Here is what std_shared_ptr<Object>(new Object) looks like:

enter image description here

The shared_ptr has a Widget*, but it's not in the same memory block as the ref counters since they were allocated separately. The Widget* was passed in, and the ref counting block was allocated internally, which is why the Widget is in a separate memory space.

On the other hand, here is what it looks like with one allocation:

enter image description here

(I'm stealing both pictures from Herb Sutter). We still need a Widget and a ref counting block, but instead the whole memory block is grabbed in a single call to new / malloc, just of sufficient size, so the Widget and ref counting block end up contiguous in memory.

Milliliter answered 22/11, 2014 at 21:48 Comment(7)
A little explaination on each of the image would be greatMelodie
@Melodie Clarified a bit.Milliliter
@Barry: H/T for clearing up that bit about the "second variable" - I didn't look at the original link posted by OP and was scratching my head about how that wasn't a shared variable.Dissection
@Deduplicator I thought answering the 2nd question was the "main" question in this question.Milliliter
MD: As Barry and @Jiří pointed out, the 2nd variable as you posted it would indeed be reference counted, the one from your link would not be. There is some good commentary along these same lines at #9201164, as well as a good treatment of STL shared pointers at channel9.msdn.com/Series/….Dissection
So the efficiency is only in construction because only one memory space is created?Is there efficiency in latter usage?Tapster
@bigxiao Yeah. Cache locality.Milliliter
H
4
  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).
    
Hamlett answered 22/11, 2014 at 21:56 Comment(0)
S
2

Why is the second one not a shared pointer ? Will that not increment a reference count

I believe the quote refers to the original poster's code, which claims to create a smart pointer but in fact does not do that. ptr_res2 is just a regular pointer.

cout << "Create smart_ptr using new..." << endl;
auto ptr_res2(new Object("new"));
cout << "Create smart_ptr using new: done." << endl;

How does make_shared only make one memory allocation and new makes two thus making make_shared more efficent

make_shared needs to allocate a slot for the counter and the object itself. It's possible to allocate the memory in one go and then use part of it for the counter and the rest for the object.

Note that this will also mean that the counter and the object itself are right next to each other in memory thus improving data locality.

Syllabify answered 22/11, 2014 at 21:46 Comment(0)
P
1
  1. The second one is still a shared pointer. It is calling the constructor for shared_ptr:

http://www.cplusplus.com/reference/memory/shared_ptr/shared_ptr/

  1. However, using make_shared is more efficient because it is only doing one allocation rather than 2 allocations. Remember, a shared_ptr needs space on the heap for Object and also the manager to keep track of the number of shared and weak pointers. If you are using the constructor, you are allocating space for Object, then passing the pointer in to the constructor which needs to allocate space for the manager. Instead, if you use make_shared, it allocates one chunk of memory that stores Object and the manager. Because allocation is relatively expensive, one allocation is better than two.
Proverb answered 22/11, 2014 at 21:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.