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 an Rc or Arc using Rc::downgrade(&rc_ptr) or Arc::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() returns Option<Rc<T>> (or Option<Arc<T>>).
  • If the data is still alive (strong count > 0 when upgrade is called), it returns Some(strong_ptr). This temporarily increments the strong count while the returned Rc/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.

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.