Search code examples
c++c++11inheritancepolymorphismshared-ptr

Will instance of shared_ptr<Base> and shared_ptr<Derived> with same raw pointer share reference count?


Let's say I have two classes, Base and Derived, where Derived inherits from Base. Now, let's say I execute the following code:

shared_ptr<Derived> derivedPtr = make_shared<Derived>();
shared_ptr<Base> basePtr = derivedPtr;

Will the copying of derivedPtr to basePtr result in derivedPtr's reference count being updated (so that derivedPtr.use_count() and basePtr.use_count() equal 2)? Or, since the two instances of shared_ptr are different types, will the two have a separate reference count that isn't shared (so that derivedPtr.use_count() and basePtr.use_count() equal 1)?


Solution

  • So shared_ptr is more than just a pointer and a reference count.

    It is a pointer and a pointer to a control block. That control block contains a strong count, a weak count, and a destruction function.

    There are 3 ways to construct a shared_ptr.

    First, you can construct it from a raw pointer. When that happens, it allocates a control block and sticks a "destroyer" function into it to destroy the raw pointer memory (delete t;).

    Second, you can use make_shared. This allocates one block with space for both the control block and the object in it. It then sets the destroyer up to just destroy the object, and not recycle the memory. The destructor of the control block cleans up both memory allocations.

    Third, there is the aliasing constructors. These share control blocks (and hence destruction code), but have a different object pointer.

    The most common aliasing constructor is the one that creates a pointer-to-base, which you are doing above. The pointer-to-base differs from the shared ptr you created it from, but the control block remains the same. So whenever the control block hits 0 strong reference counts, it destroys the object as its original derived object.

    The rarer one can be used to return shared pointers to member variables, like this:

    struct Bob {
      int x;
    };
    auto pBob = std::make_shared<Bob>();
    pBob->x = 7;
    auto pInt = std::shared_ptr<int>( pBob, &(pBob->x) );
    

    now pInt is a pointer to pBob->x that shares the reference counting of the Bob created 2 lines above (where we made pBob).

    pBob = {};
    

    now the last pointer to the Bob is gone, but the object survives, kept alive by the pInt's control block (and strong count) ownership.

    Then when we:

    pInt = {};
    

    finally the Bob is deallocated.

    The cast-to-base implicit conversion you did in your question is just a variation of this.

    This second aliasing constructor can also be used to do extremely strange things, but that is another topic.


    shared/weak ptr is one of those cases where it seems you can just "monkey code" it without understanding it, but in my experience using shared ownership is sufficiently hard that fully understanding shared ptr is (a) easier than getting shared ownership right, and (b) makes getting shared ownership right easier.