3

My question is about this claim:

If any std::weak_ptr references the control block created by std::make_shared after the lifetime of all shared owners ended, the memory occupied by T persists until all weak owners get destroyed as well, which may be undesirable if sizeof(T) is large. Source

I read here, that this object live until last weak_ptr is present. Does it free object made with make_shared, with cyclic reference to self or it will live in memory forever?

For example:

struct A
{
    std::weak_ptr<A> parent;
}

void fn()
{
    auto a=std::make_shared<A>();
    a->parent = a;
} // Will it destroy here or not?
curiousguy
  • 8,038
  • 2
  • 40
  • 58
Vizor
  • 396
  • 3
  • 14

3 Answers3

11

It is destroyed. That's one of the reason why weak_ptr exists.

When a is destroyed, the reference counter becomes 0, so the object is destroyed. That means the destructor of the object is called, which destroys a->parent too.

Don't confuse destruction with deallocation. When reference counter becomes 0, or no shared_ptr owns the object, the object is destroyed. If there is any weak_ptr which points the control block, the memory won't be deallocated - because the object was allocated with std::make_shared - but the object is definitely destroyed.

ikh
  • 10,119
  • 1
  • 31
  • 70
3

Problem involve with:

If any std::weak_ptr references the control block created by std::make_shared after the lifetime of all shared owners ended, the memory occupied by T persists until all weak owners get destroyed as well, which may be undesirable if sizeof(T) is large

is something like

std::weak_ptr<A> global;

void foo()
{
    auto a = std::make_shared<A>();
    global = a;
}

So global point to control block of a. Here even if a is destroyed, the memory used by a is still present to allow the control block to exist.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

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.

curiousguy
  • 8,038
  • 2
  • 40
  • 58