19.8 Weak<T>
: Breaking Reference Cycles
Reference-counted pointers (Rc<T>
, Arc<T>
) track ownership via a strong reference count. The data stays alive as long as the strong count > 0. This works well unless objects form a reference cycle: Object A holds a strong reference (Rc
or Arc
) to Object B, and Object B holds a strong reference back to Object A.
In such a cycle, even if all external references to A and B are dropped, A and B still hold strong references to each other. Their strong counts will never reach zero, and their memory will leak – it’s never deallocated.
Weak<T>
is a companion smart pointer for both Rc<T>
and Arc<T>
designed specifically to break these cycles. A Weak<T>
provides a non-owning reference to data managed by an Rc
or Arc
.
19.8.1 Strong vs. Weak References
- Strong Reference (
Rc<T>
/Arc<T>
): Represents ownership. Increments the strong reference count. Keeps the data alive. - Weak Reference (
Weak<T>
): Represents a non-owning, temporary reference. Created from anRc
orArc
usingRc::downgrade(&rc_ptr)
orArc::downgrade(&arc_ptr)
. It increments a separate weak reference count but does not affect the strong count. Does not keep the data alive by itself.
By using Weak<T>
for references that would otherwise complete a cycle (e.g., a child referencing its parent in a tree where parents own children), you allow the strong counts to drop to zero when external references disappear, enabling proper deallocation.
19.8.2 Accessing Data via Weak<T>
Since a Weak<T>
doesn’t own the data, the data might have been deallocated (if the strong count reached zero) while the Weak<T>
still exists. Therefore, you cannot access the data directly through a Weak<T>
.
To access the data, you must attempt to upgrade the Weak<T>
back into a strong reference (Rc<T>
or Arc<T>
) using the upgrade()
method:
weak_ptr.upgrade()
returnsOption<Rc<T>>
(orOption<Arc<T>>
).- If the data is still alive (strong count > 0 when
upgrade
is called), it returnsSome(strong_ptr)
. This temporarily increments the strong count while the returnedRc
/Arc
exists. - If the data has already been deallocated (strong count was 0), it returns
None
.
This mechanism ensures you only access the data if it’s still valid.
19.8.3 Example: Tree Structure with Parent Links
Consider a tree where nodes own their children (Rc
), but children need a reference back to their parent. Using Rc
for the parent link would create cycles. Weak
solves this:
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, // Parent link uses Weak to avoid cycles parent: RefCell<Weak<Node>>, // Children links use Rc for ownership children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), // Start with no parent children: RefCell::new(vec![]), }); println!( "Leaf initial: strong={}, weak={}", Rc::strong_count(&leaf), Rc::weak_count(&leaf) ); // Output: strong=1, weak=0 let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), // Branch owns leaf }); println!( "Branch initial: strong={}, weak={}", Rc::strong_count(&branch), Rc::weak_count(&branch) ); // Output: strong=1, weak=0 // Set leaf's parent to point to branch using a weak reference *leaf.parent.borrow_mut() = Rc::downgrade(&branch); // Creates a Weak pointer println!( "Branch after parent link: strong={}, weak={}", Rc::strong_count(&branch), Rc::weak_count(&branch) // Weak count incremented ); // Output: strong=1, weak=1 println!( "Leaf after parent link: strong={}, weak={}", Rc::strong_count(&leaf), Rc::weak_count(&leaf) // Leaf strong count is 2 (owned by `leaf` var and `branch.children`) ); // Output: strong=2, weak=0 // Access leaf's parent using upgrade() if let Some(parent_node) = leaf.parent.borrow().upgrade() { // Successfully got an Rc<Node> to the parent println!("Leaf's parent value: {}", parent_node.value); // Output: 5 } else { println!("Leaf's parent has been dropped."); } // Check counts before dropping branch println!("Counts before dropping branch: branch(strong={}, weak={}), leaf(strong={}, weak={})", Rc::strong_count(&branch), Rc::weak_count(&branch), Rc::strong_count(&leaf), Rc::weak_count(&leaf)); // Output: branch(1, 1), leaf(2, 0) drop(branch); // Drop the `branch` variable's strong reference println!( "Counts after dropping branch: leaf(strong={}, weak={})", Rc::strong_count(&leaf), // Leaf strong count drops to 1 (only `leaf` var remains) Rc::weak_count(&leaf) ); // Output: leaf(strong=1, weak=0) // Try accessing the parent again; branch data should be gone. if leaf.parent.borrow().upgrade().is_none() { println!("Leaf's parent has been dropped (upgrade failed)."); // This should print } else { println!("Leaf's parent still exists?"); // Should not print } // leaf drops here, its strong count becomes 0, Node(3) is dropped. }
By using Weak<Node>
for the parent
field, the reference cycle is broken, allowing both branch
and leaf
nodes to be deallocated correctly when their strong counts reach zero.