There is a cycle of object reachability via implementation pointers, that is, you can follow pointers used inside of the private implementations of these objects and go back to where you started: a->parent
contains a pointer to the meta information (control block) created either by std::shared_ptr
or by std::make_shared
.
Of course std::make_shared
is expected to minimize the number of dynamic memory allocations by putting together the meta information and the managed object, but that has no bearing on when the managed object is destroyed (which is the only observable aspect since no class specific operator new
/operator delete
was used). So whether the control block is collocated with the managed object, or has a pointer to that object allocated separately, is irrelevant.
In all but a handful of degenerate cases (where the smart pointer is constructed with a fake deleter that doesn't deallocate anything), the end of the lifetime of the last shared owning smart pointer causes a deleter to run, usually:
- of the form
delete p;
to run, where p
is the argument of type T*
of the constructor of std::shared_ptr<U>
,
- or of the form
p->~T()
, where p
is the result of new (buff) T
in std::make_shared<T>()
.
In either case, the value of p
can be obtained from the meta information.
[Note that the value p
to be deleted is never obtained from the U*
pointer value stored in any particular std::shared_ptr<U>
instance, as such pointer value may not be "deletable", as the destructor may not be virtual (as long as the pointer argument std::shared_ptr<U>
has the right static type: it's sufficient that delete p;
where p
is the value of the type passed to the templated constructor), as the pointer may be to a member subobject or a complete different complete object, if an aliasing constructor was used to construct another std::shared_ptr
with shared ownership.]
So having a pointer to the control block usually allows one to recover a pointer to the controlled object, although that pointer to the complete object cannot be obtained through the public interface (except that the pointer is passed to the deleter, so the only way to recover the pointer in C++, if it was lost, would be to have passed a custom deleter and wait for it to be called). The pointer can certainly be recovered by navigating inside the memory representation (although that navigation might need to use dynamic_cast
to a type unknown at compile time, something the debugger will be able to do as long as it knows about all derived classes).
So we have the cycle:
a
a->parent
parent->control_block
control_block.deleter (virtual call or stored function)
deleter.a
if the pointer is stored in a dynamically created deleter, as is necessary to create std::shared_ptr<U>(T*)
, or
a
a->parent
parent->control_block
control_block.buffer
for objects created with a single allocation make_shared
: the object was constructed inside that buffer so &control_block.buffer == a
.
But cycles of pointers are not an issue, only cycle of ownership as it implies "self ownership controlled by lifetime", that is "I will destruct myself only when my lifetime will be over" (aka "I will enter destructor when I will have entered destructor"), an absurdity.
Here there is no ownership as a weak reference only owns the meta information, not the information.