15

I read about shared pointers and understood how to use. But I never understood the cyclic dependency problem with shared pointers and how weak pointers are going to fix those issues. Can any one please explain this problem clearly?

Masked Man
  • 1
  • 7
  • 40
  • 80
kadina
  • 5,042
  • 4
  • 42
  • 83

4 Answers4

26

The problem isn't that complex. Let --> represent a shared pointer:

The rest of the program  --> object A --> object B
                                    ^     |
                                     \    |
                                      \   v
                                        object C

So we've got ourselves a circular dependency with shared pointers. What's the reference count of each object?

A:  2
B:  1
C:  1

Now suppose the rest of the program (or at any rate the part of it that holds a shared pointer to A) is destroyed. Then the refcount of A is reduced by 1, so the reference count of each object in the cycle is 1. So what gets deleted? Nothing. But what do we want to be deleted? Everything, because none of our objects can be reached from the rest of the program any more.

So the fix in this case is to change the link from C to A into a weak pointer. A weak pointer doesn't affect the reference count of its target, which means that when the rest of the program releases A, its refcount hits 0. So it's deleted, hence so is B, hence so is C.

Before the rest of the program releases A, though, C can access A whenever it likes by locking the weak pointer. This promotes it to a shared pointer (and increases the refcount of A to 2) for as long as C is actively doing stuff with A. That means if A is otherwise released while this is going on then its refcount only falls to 1. The code in C that uses A doesn't crash, and A is deleted whenever that short-term shared pointer is destroyed. Which is at the end of the block of code that locked the weak pointer.

In general, deciding where to put the weak pointers might be complex. You need some kind of asymmetry among the objects in the cycle in order to choose the place to break it. In this case we know that A is the object referred to by the rest of the program, so we know that the place to break the cycle is whatever points to A.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • Thanks Steve. I understood it now. Thanks for the clear explanation. – kadina Mar 05 '14 at 02:07
  • 1
    Not sure if this is correct example to explain cyclic dependency. Reference count is maintained within Control block and all shared ptrs A, B, C will point to same control block, with count = 3. – Manish Baphna May 31 '21 at 12:23
  • 1
    @ManishBaphna no, there are three control blocks, one each for objects A, B, and C. there are (initially) four (unlabeled) shared pointers in the example. – Caleth Mar 17 '22 at 10:12
10
shard_ptr<A> <----| shared_ptr<B> <------
    ^             |          ^          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
class A           |     class B         |
    |             |          |          |
    |             ------------          |
    |                                   |
    -------------------------------------

Now if we make the shared_ptr of the class B and A, the use_count of the both pointer is two.

When the shared_ptr goes out od scope the count still remains 1 and hence the A and B object does not gets deleted.

class B;

class A
{
    shared_ptr<B> sP1; // use weak_ptr instead to avoid CD

public:
    A() {  cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void setShared(shared_ptr<B>& p)
    {
        sP1 = p;
    }
};

class B
{
    shared_ptr<A> sP1;

public:
    B() {  cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }

    void setShared(shared_ptr<A>& p)
    {
        sP1 = p;
    }
};

int main()
{
    shared_ptr<A> aPtr(new A);
    shared_ptr<B> bPtr(new B);

    aPtr->setShared(bPtr);
    bPtr->setShared(aPtr);

    return 0;  
}

output:

A()
B()

As we can see from the output that A and B pointer are never deleted and hence memory leak.

To avoid such issue just use weak_ptr in class A instead of shared_ptr which makes more sense.

Swapnil
  • 1,424
  • 2
  • 19
  • 30
  • 1
    I think it's not enough for the explanation. What will happen if the object of the class ```B``` is destructed? In "for dummy" explanation it destroys the object within itself. So it invokes the destruction of ```shared_ptr``` object that leads to the destruction of object of the class ```A```. Okay, ```A``` destroys the ```shared_ptr``` that points to the ```B's``` object. Which state of ```B``` will see ```A```: destroyed/half-destroyed/still exists? And what's then? Why this behaviour is ignored on a run? Why doesn't it throw an exception? – DisplayName Sep 13 '20 at 14:59
0

If you are aware of the cyclic dependency then you can stick with shared_ptr without switching to weak_ptr but the deletion of objects require some manual work. The following code is modified from Swapnil's answer.

#include <iostream>
#include <memory>

using namespace std ;

class B;

class A
{
   shared_ptr<B> sP1; // use weak_ptr instead to avoid CD

public:
   A() {  cout << "A()" << endl; }
   ~A() { cout << "~A()" << endl; }

   void setShared(shared_ptr<B>& p)
   {
       sP1 = p;
   }

   // nullifySharedPtr cuts the circle of reference
   // once this is triggered, then the ice can be broken
   void nullifySharedPtr() {
      sP1 = nullptr; 
   }

};

class B
{
   shared_ptr<A> sP1;

public:
   B() {  cout << "B()" << endl; }
   ~B() { cout << "~B()" << endl; }

   void setShared(shared_ptr<A>& p)
   {
       sP1 = p;
   }
};

int main()
{
   shared_ptr<A> aPtr(new A);
   shared_ptr<B> bPtr(new B);
   
   aPtr->setShared(bPtr);
   bPtr->setShared(aPtr);

   cout << aPtr.use_count() << endl;
   cout << bPtr.use_count() << endl;

   // to be break the ice:
   aPtr->nullifySharedPtr() ;
   
   return 0;  
}

nullifySharedPtr acts as a scissor to cut the circle and hence enables the system to do its own job of deletion.

roy.atlas
  • 411
  • 2
  • 8
0

The Issue itself is displayed above. Solutions are

  • Manual breaking according to roy.atlas
  • Assert by design, that you have a tree structure, without cycles, e.g. in a XML-DOM tree, you would put the parent relation as weak pointer
  • In a generic solution an object will exist, as long as there is a chain of shared_ptr-s from a local or static variable, i.e. from any object, which is not on the heap. For this you need to be able to detect the memory segment type of an object, which depends on the runtime environment; also each shared_ptr-member of a class must know the surrounding instance. Now, when a shared_ptr is deleted, the referenced object can find an alternative path from a non-heap object or it will be destroyed.
Sam Ginrich
  • 661
  • 6
  • 7