10

The enable_shared_from_this helper contains a weak pointer that is set when creating shared pointer to the object. That means there is the reference-count (allocated separately or together with the object using make_shared) and an extra weak_ptr in the object.

Now why doesn't it simply contain the reference count instead? When setting shared_ptr from dumb pointer, the type has to be fully defined, so the shared_ptr constructor or assignment operator can detect the type is derived from enable_shared_from_this and use the correct counter and the format can remain the same, so copying does not care. In fact, the shared_ptr already has to detect it to set the embedded weak_ptr.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Jan Hudec
  • 73,652
  • 13
  • 125
  • 172

3 Answers3

7

The first thing that comes to mind is whether that approach would be feasible at all, and the answer is that it would not:

struct X : enable_shared_from_this {};
std::shared_ptr<X> p( new X );
std::weak_ptr<X> w( p );
p.reset();                      // this deletes the object
if ( w.use_count() ) {          // this needs access to the count object
                                //    but it is gone! Undefined Behavior

If the count is stored in the object, then no weak_ptr can outlive the object, which is a breach in the contract. The whole idea of weak_ptr is that they can outlive the object (if the last shared_ptr goes out of scope, the object is deleted even if there are weak_ptr)

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • 1
    I originally thought the weak pointers could form a linked list and be reset when the object is being deleted, but doing it in a thread-safe way is obviously such a huge can of worm that it's not really viable. – Jan Hudec Jul 26 '11 at 11:10
  • @Jan Hudec: Wherever the each `weak_ptr` is stored there is a piece of shared data: *is the object alive*, that data is in the `count` object and cannot be deleted before the last `weak_ptr` goes out of scope/is deleted. Note that `weak_ptr` are **not** released when the object dies, they can outlive the pointed to object (the whole purpose is that they can). I don't think multithreading has anything to do with this issue. – David Rodríguez - dribeas Jul 26 '11 at 12:37
  • 1
    Yes, I realize they are implemented that way. The issue with threads is: You can implement weak pointers so that all weak pointers to the object are connected to a linked list and you zero them all when the objects is being destroyed, so there is no shared piece that would have to stay around. However manipulating the list would require a lot of locking which would make that approach extremely slow in C++ (while it can be used efficiently in environments with stop-the-world garbage collection or those with big interpreter lock anyway like python). – Jan Hudec Jul 26 '11 at 13:38
  • You could keep the data structure around, and only destroy the object. Not nice of the sizeof object is huge. You could select different implementations based on sizeof. – curiousguy Oct 08 '11 at 17:09
  • @curiousguy: What you are saying is that the reference count would be allocated together, but not part of the object. You cannot really do it with a *base class*, either the reference count is part of the class or it is not. Because destruction of the *object* implies destruction of the base classes, that implies that the reference count cannot be embedded in the `enable_shared_from_this`. The advantage, on the other hand would be contiguous allocation, and that can already be achieved by using `make_shared` (single allocation, two objects: reference count/real object). – David Rodríguez - dribeas Oct 08 '11 at 20:10
  • Hum... You could embed the raw storage for the `shared_ptr` internal data structure (how is called?) in `enable_shared_from_this`. Acrobatic, but fun. – curiousguy Oct 08 '11 at 20:23
  • @DavidRodríguez-dribeas This was the origin of my question: [After an object is destroyed, what happens to subobjects of scalar type?](https://stackoverflow.com/q/11637611/963864) – curiousguy Aug 31 '19 at 19:14
1

Separation of concerns: make_shared to embed the count, enable_shared_from_this for shared_from_this.

There's no reason the two should be intermingled: the library can't assume what the client code has for requirements. By separating the two, client code can pick and choose whatever fits best.

In addition Boost (where shared_ptr comes from) also proposes intrusive_ptr.

(Consider that your suggestion doesn't seem to allow for custom deleters. You could fix that by changing enable_shared_from_this to template<typename T, typename Deleter = default_deleter<T>> class enable_shared_from_this; but by this point it's coming close to reinventing intrusive_ptr.)

Luc Danton
  • 34,649
  • 6
  • 70
  • 114
  • 1
    If `enable_shared_from_this` embedded the count, it would save space, so it would be more efficient. Separation of concerns is not a reason to implement it in a less efficient way. And I don't think it prevents custom deleter passed the normal way (the `shared_ptr` constructor would just need to wrap it slightly differently because it should not be deleting the reference count). – Jan Hudec Jul 26 '11 at 09:15
  • @Jan Have you considered that deleters might have state and have varying sizes? If you do `shared_ptr(new enabled_type, some_deleter())` how many allocations are done with your scheme? If the deleter is separately allocated, does it really matter whether the reference count is embedded inside the object or lives with the deleter? – Luc Danton Jul 26 '11 at 09:21
  • 1
    Hm, you are right that the generic deleter case is quite hairy. It could still be more efficient in the common case with default deleter though. – Jan Hudec Jul 26 '11 at 09:30
  • @Jan Which is why I mentioned separation of concerns in the first place: `make_shared` do put some restrictions (i.e. no custom deleters), it's not an optimization that is available for each situation. Just like what you're suggesting. – Luc Danton Jul 26 '11 at 09:33
  • @Luc Danton: I have not read the implementation of the `shared_ptr` but I would expect it to perform type erasure on the deleter, which means that the type-erased object will be the same in all count objects. – David Rodríguez - dribeas Jul 26 '11 at 09:55
  • @David That's what it does yes. What's relevant is where is it stored in the current implementation vs Jan's suggestion. – Luc Danton Jul 26 '11 at 10:04
  • @DavidRodríguez-dribeas "_I have not read the implementation of the shared_ptr_" It currently uses a virtual function; I vaguely remember that it used to be implemented with a function pointer for efficiency. – curiousguy Oct 10 '11 at 19:30
  • @curiousguy: That is what is called *type-erasure*, the actual *deleter* is hidden in the implementation details, and from the point of view of `shared_ptr`, the exact type is irrelevant, the type has been erased. Type erasure is usually implemented in terms of a base with a virtual function and one or more derived class templates that are dynamically instantiated, holding a pointer/reference to the base type hides the exact instantiation of the template that is in use. – David Rodríguez - dribeas Oct 10 '11 at 19:52
1

To get any advantage from embedding the count in the object, you'ld need to do away with the second pointer in the shared_ptr, which would change its layout, and also create problems for creating the destructor object. If you change the layout, then this change must be visible everywhere the shared_ptr is used. Which means you couldn't have an instance of the shared_ptr pointing to an incomplete type.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • No, I would not. Copying the extra pointer around is cheap. The expensive thing is allocation. – Jan Hudec Jul 26 '11 at 13:40
  • Could you explain the problems? – curiousguy Oct 08 '11 at 17:16
  • @JanHudec A lot depends on the compiler; in an extreme case (but I don't know of such a compiler), a `shared_ptr` with a single pointer could be passed in a register, a `shared_ptr` with two pointers wouldn't. Which would make a radical difference. More generally, copying two pointers will cost more than copying one. But you're right that the allocation is expensive, and another important issue is locality---a separate counter is likely in a different cache line, which will increase cache hits. So even with the second pointer, you could gain a lot. – James Kanze Oct 10 '11 at 07:56
  • @curiousguy I'm not sure which problems you're talking about. There's no place to put the destructor object if you embed the counter in the object, rather than allocating a separate counter-destructor, as `boost::shared_ptr` does. – James Kanze Oct 10 '11 at 07:58
  • You'd put exactly the same thing in the object that you allocate separately, so no, that's not an argument. – Jan Hudec Oct 10 '11 at 08:12
  • @JamesKanze "_There's no place to put the destructor object_" You could have a char array and only apply the optimisation if the functor fits in (think: short string optimisation). That is not the real problem. The real problem is support for weak count: you would need to separate object destruction (which is observable behaviour) and memory deallocation (and memory deallocation must not be part of the observable behaviour of `shared_ptr`). – curiousguy Oct 10 '11 at 19:23
  • @curiousguy Not a real problem is relative; it would certainly add significant complexity to the pointer. And the support for weak pointers is another good point. – James Kanze Oct 11 '11 at 07:21
  • @JamesKanze All this discussion convinced me that the general case cannot be optimised. But I am not sure that the case where the destructor is _not_ virtual, and there is no overloaded `operator delete`, and the one-argument `share_ptr` constructor is used cannot be optimised so that there is no allocation **until** the `shared_ptr` is converted to `weak_ptr`. – curiousguy Oct 11 '11 at 13:26