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 A
s.
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_ptr
s 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
Restrict Variadic Template Arguments
Constructor Initialization VS Assignment
Who Defines C Operator Precedence and Associativity
Comma Operator in If Condition
Why Don't Std::Vector's Elements Need a Default Constructor
Comparing 3 Modern C++ Ways to Convert Integral Values to Strings
Implicit Conversion When Overloading Operators for Template Classes
How to Rotate a N X N Matrix by 90 Degrees
C++ Read File Line by Line Then Split Each Line Using the Delimiter
Win32: How to Hide 3Rd Party Windows in Taskbar by Hwnd
Is There a C++ Iterator That Can Iterate Over a File Line by Line
Gcc: Difference Between -O3 and -Os
How to "Normalize" a Pathname Using Boost::Filesystem
Segfaults in Malloc() and Malloc_Consolidate()