How to Break Shared_Ptr Cyclic Reference Using Weak_Ptr

How to break shared_ptr cyclic reference using weak_ptr

The classic example of cyclic references is where you have two classes A and B where A has a reference to B which has a reference to A:

#include <memory>
#include <iostream>

struct B;
struct A {
std::shared_ptr<B> b;
~A() { std::cout << "~A()\n"; }
};

struct B {
std::shared_ptr<A> a;
~B() { std::cout << "~B()\n"; }
};

void useAnB() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;
}

int main() {
useAnB();
std::cout << "Finished using A and B\n";
}

If both references are shared_ptr then that says A has ownership of B and B has ownership of A, which should ring alarm bells. In other words, A keeps B alive and B keeps A alive.

In this example the instances a and b are only used in the useAnB() function so we would like them to be destroyed when the function ends but as we can see when we run the program the destructors are not called.

The solution is to decide who owns who. Lets say A owns B but B does not own A then we replace the reference to A in B with a weak_ptr like so:

struct B {
std::weak_ptr<A> a;
~B() { std::cout << "~B()\n"; }
};

Then if we run the program we see that a and b are destroyed as we expect.

Live demo

Edit: In your case, the approach you suggested looks perfectly valid. Take ownership away from A and something else owns the As.

Plain reference instead of weak_ptr to break circular dependency

Here's a slightly modified function:

void useAnB() {
std::shared_ptr<B> oops;
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;
oops = b;
}
// use oops->a
}

How could you know that oops->a no longer refers to a valid object if it was a plain pointer or reference?

Breaking cyclic references with std::weak_ptr and alias constructor: sound or problematic?

The shared_ptr<Node> returned by your getParent owns the parent, not the parent's parent.

Thus, calling getParent again on that shared_ptr can return an empty (and null) shared_ptr. For example:

int main() {
auto gp = std::make_shared<Node>();
auto p = gp->getOrCreateLeft();
auto c = p->getOrCreateLeft();
gp.reset();
p.reset(); // grandparent is dead at this point
assert(c->getParent());
assert(!c->getParent()->getParent());
}

(The inherited shared_from_this also passes out shared_ptrs that owns the node rather than its parent, but I suppose you can make it harder to mess up by a private using declaration and ban it by contract.)

Use of weak_ptr with cyclic references

With all std::shared_ptr, you have:

int main() {
std::shared_ptr<A> a = std::make_shared<A>(); // ref_count_a = 1
std::shared_ptr<B> b = std::make_shared<B>(); // ref_count_b = 1
a->set_B(b); // ref_count_b = 2
b->set_A(a); // ref_count_a = 2
} // ref_count_a = 1 && ref_count_b = 1
// memleak due to the cycle

With

class B {
std::weak_ptr<A> a_ptr;
// ...
};

it becomes:

int main() {
std::shared_ptr<A> a = std::make_shared<A>(); // ref_count_a = 1
std::shared_ptr<B> b = std::make_shared<B>(); // ref_count_b = 1
a->set_B(b); // ref_count_b = 2
b->set_A(a); // ref_count_a = 1 , weak_ref_a = 1
} // ref_count_a = 0 && ref_count_b = 1
// -> release a -> ref_count_b = 0
// -> release b (+ control block) -> weak_ref_a = 0
// -> release control block of a

Also it mentions that a weak_ptr breaks strong ownership reference but how can a weak_ptr have no ownership, how will it access the object?

The control maintains a counter for the shared_ptr (to release the object)
and a counter for weak_ptr to release the control block.
weak_ptr retrieves the shared_ptr thanks to control block.

Finally I heard that a weak_ptr is a smart pointer which can use methods such as lock() or expired() to manage a shared_ptr, again is this correct?

Yes

What is the cyclic dependency issue with shared_ptr?

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.

why weak_ptr can break cyclic reference?

It is not included in the reference count, so the resource can be freed even when weak pointers exist. When using a weak_ptr, you acquire a shared_ptr from it, temporarily increasing the reference count. If the resource has already been freed, acquiring the shared_ptr will fail.

Q2: shared_ptr is a strong pointer. As long as any of them exist, the resource cannot be freed.

std::make_shared(), std::weak_ptr and cyclic references

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.



Related Topics



Leave a reply



Submit