30

As illustrated in the code here, the size of the object returned from make_shared is two pointers.

However, why doesn't make_shared work like the following (assume T is the type we're making a shared pointer to):

The result of make_shared is one pointer in size, which points to of allocated memory of size sizeof(int) + sizeof(T), where the int is a reference count, and this gets incremented and decremented on construction/destruction of the pointers.

unique_ptrs are only the size of one pointer, so I'm not sure why shared pointer needs two. As far as I can tell, all it needs a reference count, which with make_shared, can be placed with the object itself.

Also, is there any implementation that is implemented the way I suggest (without having to muck around with intrusive_ptrs for particular objects)? If not, what is the reason why the implementation I suggest is avoided?

Clinton
  • 22,361
  • 15
  • 67
  • 163
  • 3
    Side note: The `count` is not just a count, there is at the very least two counts, one for the `shared_ptr`s, and another one for the `weak_ptr`s. Depending on the implementation there might be some extra memory in use. – David Rodríguez - dribeas Jul 26 '11 at 07:15
  • @David: Why the need to count `weak_ptr`s? I thought the point of a `weak_ptr` is to break cycles, so `weak_ptr` references do not keep a object alive (hence, I don't see the need to maintain a reference count of them). – Clinton Jul 26 '11 at 07:29
  • 3
    @Clinton: `weak_ptr` need access to the count to be able to determine whether the object is still alive or the last `shared_ptr` went away and the object has been deleted. That means that the `count` object must be kept alive as long as there is at least one `weak_ptr` that refers to it. The second count does not control lifetime of the object, but lifetime of the count itself. As James correctly points out, in `std::shared_ptr` (or `boost::shared_ptr`) the `count` object also owns the deleter, so that is one more piece of information to keep outside of the original object. – David Rodríguez - dribeas Jul 26 '11 at 07:38
  • @David: I see, thanks for the explanation of `weak_ptr`, though I still can't see why objects from `make_shared` need two pointers, as I believe their difference would be a compile time constant (besides the standard perhaps requiring them to be the same type). – Clinton Jul 26 '11 at 08:37
  • @Clinton: Ok, lets follow that rationale: assume that `make_shared` returned an object of a type `MS` that held a single pointer. The type itself has knowledge of the offset (it knows how much space is required by the `count`). Now, the use of `make_shared` is: `shared_ptr p = make_shared();` (or any variant). `make_shared` would create a `MS` object that holds a single pointer. The very next step is the initialization of `p`, which would convert the `MS` temporary into a `shared_ptr` with 2 pointers. Now, why would you create a type `MS` in the first place? There is no real gain. – David Rodríguez - dribeas Jul 26 '11 at 09:17
  • @David: With `make_shared` you don't get to specify a deleter. That's part of the reason that it's more efficient, because it just uses the allocator for the entire refcontrol-plus-object block. A great description of the interals of `shared_ptr` can be found in [STL #1](http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stephan-T-Lavavej-Advanced-STL-1-of-n). – Kerrek SB Jul 26 '11 at 10:15
  • @Kerrek SB: I don't think I follow how your comment, or how it relates to my comments. The `shared_ptr` **needs** the deleter, and *yes*, you could create a different type in `make_shared` that is implicitly convertible to `shared_ptr`, and you can make that type have a single pointer, but the lifetime of that object will be *very short*: the very first thing that is done with the result of `make_shared` is assign it to (or initialize) a `shared_ptr` which requires the extra pointer. At the end of the `shared_ptr` lifetime it will call the *deleter*. – David Rodríguez - dribeas Jul 26 '11 at 12:43
  • ... unless you intend to have the result of `make_shared` incompatible with `shared_ptr`. At the end of the expression, the `shared_ptr` has to be a `shared_ptr`, with `deleter` and all. (There is no alternative: either at the end of the expression you get a `shared_ptr` with all that is required by the `shared_ptr` or you don't) – David Rodríguez - dribeas Jul 26 '11 at 12:45
  • @David: I'm a bit confused myself now, but what I wanted to point out is that not every shared pointer has a deleter. The count object is polymorphic and has an abstract "delete" function, but that one is implemented differently in the three concrete cases "default" (call `delete`), "custom deleter" (call custom deleter) and "make_shared" (`delete` entire control block). – Kerrek SB Jul 26 '11 at 13:01

4 Answers4

47

In all implementations I'm aware of, shared_ptr stores the owned pointer and the reference count in the same memory block. This is contrary to what other answers are saying. Additionally a copy of the pointer will be stored in the shared_ptr object. N1431 describes the typical memory layout.

It is true that one can build a reference counted pointer with sizeof only one pointer. But std::shared_ptr contains features that absolutely demand a sizeof two pointers. One of those features is this constructor:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept;

    Effects: Constructs a shared_ptr instance that stores p
             and shares ownership with r.

    Postconditions: get() == p && use_count() == r.use_count()

One pointer in the shared_ptr is going to point to the control block owned by r. This control block is going to contain the owned pointer, which does not have to be p, and typically isn't p. The other pointer in the shared_ptr, the one returned by get(), is going to be p.

This is referred to as aliasing support and was introduced in N2351. You may note that shared_ptr had a sizeof two pointers prior to the introduction of this feature. Prior to the introduction of this feature, one could possibly have implemented shared_ptr with a sizeof one pointer, but no one did because it was impractical. After N2351, it became impossible.

One of the reasons it was impractical prior to N2351 was because of support for:

shared_ptr<B> p(new A);

Here, p.get() returns a B*, and has generally forgotten all about the type A. The only requirement is that A* be convertible to B*. B may derive from A using multiple inheritance. And this implies that the value of the pointer itself may change when converting from A to B and vice-versa. In this example, shared_ptr<B> needs to remember two things:

  1. How to return a B* when get() is called.
  2. How to delete a A* when it is time to do so.

A very nice implementation technique to accomplish this is to store the B* in the shared_ptr object, and the A* within the control block with the reference count.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 3
    Ok. This explains why `shared_ptr` deletes object correctly even if the destructor of B is not virtual. (this is for the case `shared_ptr p(new A);` and `class A : B;`) – Ivan_a_bit_Ukrainivan Jun 14 '18 at 12:50
3

The reference count cannot be stored in a shared_ptr. shared_ptrs have to share the reference count among the various instances, therefore the shared_ptr must have a pointer to the reference count. Also, shared_ptr (the result of make_shared) does not have to store the reference count in the same allocation that the object was allocated in.

The point of make_shared is to prevent the allocation of two blocks of memory for shared_ptrs. Normally, if you just do shared_ptr<T>(new T()), you have to allocate memory for the reference count in addition to the allocated T. make_shared puts this all in one allocation block, using placement new and delete to create the T. So you only get one memory allocation and one deletion.

But shared_ptr must still have the possibility of storing the reference count in a different block of memory, since using make_shared is not required. Therefore it needs two pointers.

Really though, this shouldn't bother you. Two pointers isn't that much space, even in 64-bit land. You're still getting the important part of intrusive_ptr's functionality (namely, not allocating memory twice).


Your question seems to be "why should make_shared return a shared_ptr instead of some other type?" There are many reasons.

shared_ptr is intended to be a kind of default, catch-all smart pointer. You might use a unique_ptr or scoped_ptr for cases where you're doing something special. Or just for temporary memory allocations at function scope. But shared_ptr is intended to be the sort of thing you use for any serious reference counted work.

Because of that, shared_ptr would be part of an interface. You would have functions that take shared_ptr. You would have functions that return shared_ptr. And so on.

Enter make_shared. Under your idea, this function would return some new kind of object, a make_shared_ptr or whatever. It would have its own equivalent to weak_ptr, a make_weak_ptr. But despite the fact that these two sets of types would share the exact same interface, you could not use them together.

Functions that take a make_shared_ptr could not take a shared_ptr. You might make make_shared_ptr convertible to a shared_ptr, but you couldn't go the other way around. You wouldn't be able to take any shared_ptr and turn it into a make_shared_ptr, because shared_ptr needs to have two pointers. It can't do its job without two pointers.

So now you have two sets of pointers which are half-incompatible. You have one-way conversions; if you have a function that returns a shared_ptr, the user had better be using a shared_ptr instead of a make_shared_ptr.

Doing this for the sake of a pointer's worth of space is simply not worthwhile. Creating this incompatibility, creating two sets of pointers just for 4 bytes? That simply isn't worth the trouble that is caused.

Now, perhaps you would ask, "if you have make_shared_ptr why would you ever need shared_ptr at all?"

Because make_shared_ptr is insufficient. make_shared is not the only way to create a shared_ptr. Maybe I'm working with some C-code. Maybe I'm using SQLite3. sqlite3_open returns a sqlite3*, which is a database connection.

Right now, using the right destructor functor, I can store that sqlite3* in a shared_ptr. That object will be reference counted. I can use weak_ptr where necessary. I can play all the tricks I normally would with a regular C++ shared_ptr that I get from make_shared or whatever other interface. And it would work perfectly.

But if make_shared_ptr exists, then that doesn't work. Because I can't create one of them from that. The sqlite3* has already been allocated; I can't ram it through make_shared, because make_shared constructs an object. It doesn't work with already existing ones.

Oh sure, I could do some hack, where I bundle the sqlite3* in a C++ type who's destructor will destroy it, then use make_shared to create that type. But then using it becomes much more complicated: you have to go through another level of indirection. And you have to go through the trouble of making a type and so forth; the destructor method above at least can use a simple lambda function.

Proliferation of smart pointer types is something to be avoided. You need an immobile one, a movable one, and a copyable shared one. And one more to break circular references from the latter. If you start to have multiple ones of those types, then you either have very special needs or you are doing something wrong.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Thanks for the answer. I now understand there is no technical reason for a shared_ptr from make_shared_ptr to have two pointers, besides compatibility with other shared_ptrs. I guess we could have 3 shared_ptrs, the default one, the one I mentioned, and one with one pointer with a pointer to the object stored in the control block. Converting between all of them could get confusing. – Clinton Jul 26 '11 at 11:32
  • "_these two sets of types would share the exact same interface,_" Wrong. `make_shared_ptr` couldn't support aliasing (in particular, arbitrary casting). `make_shared` is irrelevant here. – curiousguy Oct 30 '11 at 04:12
  • @curiousguy: You know, you could have just edited the post and changed "share the exact same interface" to "have virtually identical interfaces". No need for the down-vote. – Nicol Bolas Oct 30 '11 at 05:25
  • Hug? You want me to edit your answer to remove every mention of `make_shared`??? Aliasing is a very important functionality of `shared_ptr`. And BTW, `weak_ptr` doesn't "break cycles" and isn't even a smart pointer. And I don't agree that "_Proliferation of smart pointer types is something to be avoided._". OTOH, proliferation of smart pointer types that are used in interfaces and have the same purpose with a very slightly different space/time trade-of is to be avoided. – curiousguy Oct 30 '11 at 06:58
  • I also strongly disagree with "_`shared_ptr` is intended to be a kind of default ... pointer_". The default should be `unique_ptr`, unless you really need `shared_ptr`. – curiousguy Oct 30 '11 at 06:58
  • For example, I feel the need for a family of `shared_ptr` with a Deleter template parameter. – curiousguy Oct 30 '11 at 07:15
  • 1
    @curiousguy: I think you're misunderstanding what Clinton was asking about. He was asking why the pointer type that `make_shared` returns is a `shared_ptr` and not some other kind of pointer which could be smaller (one pointer in size rather than two). `make_shared` is thus an important part of any answer to that question, so it is most certainly not irrelevant. The part you were concerned with was the "exact same interface" part, not the relevance of `make_shared`. – Nicol Bolas Oct 30 '11 at 07:42
  • 1
    @curiousguy: What exactly is "wrong" about his question? I don't agree with his belief that it should return something else, but there's nothing wrong with asking why it doesn't. And most important of all, if you think his question is "wrong", why are you arguing with _me_ about _my_ answer to his question? `make_shared` is indeed relevant, because that's what he asked about. If you don't like what Clinton asked about, take it up with him. – Nicol Bolas Oct 30 '11 at 08:41
  • "_What exactly is "wrong" about his question?_" The question is (implicitly) incorrect: it mentions `make_shared`, implicitly opposed to `shared_ptr(new T)`, which is not the real issue. "`make_shared` _is indeed relevant, because that's what he asked about._" Actually he wants to know why there isn't a single word shared smart pointer. The answer is that it is possible, but this shared smart pointer type would not allow the arbitrary pointer conversions allowed by `shared_ptr`. – curiousguy Oct 30 '11 at 23:23
  • _The answer is that it is possible, but this shared smart pointer type would not allow the arbitrary pointer conversions allowed by 'shared_ptr'_ This is wrong. These requirements are possible with a single word shared pointer, only impractical in the general case. 'make_shared' however is not the general case and would make an efficient implementation possible. – Fabio Fracassi Oct 31 '11 at 10:42
2

I have a honey::shared_ptr implementation that automatically optimizes to a size of 1 pointer when intrusive. It's conceptually simple -- types that inherit from SharedObj have an embedded control block, so in that case shared_ptr<DerivedSharedObj> is intrusive and can be optimized. It unifies boost::intrusive_ptr with non-intrusive pointers like std::shared_ptr and std::weak_ptr.

This optimization is only possible because I don't support aliasing (see Howard's answer). The result of make_shared can then have 1 pointer size if T is known to be intrusive at compile-time. But what if T is known to be non-intrusive at compile-time? In this case it's impractical to have 1 pointer size as shared_ptr must behave generically to support control blocks allocated both alongside and separately from their objects. With only 1 pointer the generic behavior would be to point to the control block, so to get at T* you'd have to first dereference the control block which is impractical.

Qarterd
  • 280
  • 3
  • 5
1

Others have already said that shared_ptr needs two pointers because it has to point to the reference count memory block and the Pointed to Types memory Block.

I guess what you are asking is this:

When using make_shared both memory blocks are merged into one, and because the blocks sizes and alignment are known and fixed at compile time one pointer could be calculated from the other (because they have a fixed offset). So why doesn't the standard or boost create a second type like small_shared_ptr which does only contain one pointer. Is that about right?

Well the answer is that if you think it through it quickly becomes a large hassle for very little gain. How do you make the pointers compatible? One direction, i.e. assigning a small_shared_ptr to a shared_ptr would be easy, the other way round extremely hard. Even if you solve this problem efficiently, the small efficiency you gain will probably be lost by the to-and-from conversions that will inevitably sprinkle up in any serious program. And the additional pointer type also makes the code that uses it harder to understand.

Fabio Fracassi
  • 3,791
  • 1
  • 18
  • 17
  • 1
    As others have said, this has nothing to do with `make_shared`. – curiousguy Oct 30 '11 at 04:07
  • What @Nicol says. 'make_shared' is highly relevant to the question that was asked, and my answer and Nicols are both correct, 'make_shared' could return a shared pointer that has only one pointer, and the reason why it is not done is that it needs to be compatible with 'shared_ptr' which has further requirements which make this impractical, or as Howard explained even impossible. – Fabio Fracassi Oct 30 '11 at 09:43
  • If you drop support for pointer conversions (implicit derived to base or explicit casts), all Howard' arguments disappear. Then you can have `sizeof(shared_ptr) == sizeof(T*)`. – curiousguy Oct 30 '11 at 23:37
  • If you read @Howard 's answer carefully you will find that the only requirement that cannot be fulfilled with a "one ptr" smart pointer is Aliasing, which - to come back to the original question - cannot be done with 'make_shared' anyway. All other requirements can be fulfilled, albeit in an impractical (i.e. less efficient or harder to implement) way. – Fabio Fracassi Oct 31 '11 at 10:33