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)?
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.