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 because their Drop implementations are never called.

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 stored alongside the data on the heap. 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 (also stored alongside the data and the strong count on the heap) but does not affect the strong count. Does not keep the data alive by itself.

When an Rc/Arc is dropped, it decrements the strong count. If the strong count becomes zero, the inner value T is dropped. Then, the weak count is decremented (corresponding to the allocation itself). If the weak count also becomes zero, the heap allocation (holding the counts and formerly T) is freed. Weak<T> pointers only keep the allocation alive (containing the counts) until the last Weak<T> is dropped, even if T itself has already been dropped.

By using Weak<T> for references that would otherwise complete a cycle (e.g., a child referencing its parent in a tree where parents strongly own children), you allow the strong counts to drop to zero when external references disappear, enabling proper deallocation of the contained value T.

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 atomically increments the strong count and returns Some(strong_ptr). You hold a temporary strong reference.
  • If the data has already been dropped (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 (parent -> child and child -> parent both strong). 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>>, // Weak pointer doesn't own the parent
    // Children links use Rc for ownership
    children: RefCell<Vec<Rc<Node>>>, // Rc owns the children
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()), // Start with no parent (empty Weak)
        children: RefCell::new(vec![]),
    });

    println!(
        "Leaf initial: strong={}, weak={}",
        Rc::strong_count(&leaf), // Count for `leaf` variable
        Rc::weak_count(&leaf) // Count related to allocation itself + any Weak pointers
    ); // Output: strong=1, weak=1

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]), //Branch owns leaf (strong ref)
    });

    println!(
        "Branch initial: strong={}, weak={}",
        Rc::strong_count(&branch),
        Rc::weak_count(&branch)
    ); // Output: strong=1, weak=1
    println!(
        "Leaf counts after branch owns it: strong={}, weak={}",
        Rc::strong_count(&leaf), // Now 2: `leaf` var + `branch.children`
        Rc::weak_count(&leaf)
    ); // Output: strong=2, weak=1

    // Set leaf's parent to point to branch using a weak reference
    // Rc::downgrade creates a Weak<Node> from the Rc<Node>
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
    // Increments branch's weak count

    println!(
        "Branch after parent link: strong={}, weak={}",
        Rc::strong_count(&branch), // Strong count unchanged
        Rc::weak_count(&branch)
        // Weak count increments (now 2: allocation + leaf.parent)
    ); // Output: strong=1, weak=2
    println!(
        "Leaf after parent link: strong={}, weak={}",
        Rc::strong_count(&leaf), // Unchanged
        Rc::weak_count(&leaf)
    ); // Output: strong=2, weak=1

    // Access leaf's parent using upgrade()
    if let Some(parent_node) = leaf.parent.borrow().upgrade() {
        // Successfully got a temporary Rc<Node> to the parent
        println!("Leaf's parent value: {}", parent_node.value); // Output: 5
        // `parent_node` (the temporary Rc) drops here, decr. branch's strong count
    } else {
        println!("Leaf's parent has been dropped.");
    }

    // Check counts before dropping branch variable
    println!("Counts before dropping branch var: branch(strong={}, weak={}),
        leaf(strong={}, weak={})",
        Rc::strong_count(&branch), Rc::weak_count(&branch), // branch(1, 2)
        Rc::strong_count(&leaf), Rc::weak_count(&leaf));    // leaf(2, 1)

    drop(branch); // Drop the `branch` variable's strong reference

    println!(
        "Counts after dropping branch var: leaf(strong={}, weak={})",
        Rc::strong_count(&leaf),
        // Leaf strong count drops to 1 (only `leaf` var remains)
        Rc::weak_count(&leaf)  // Leaf weak count remains 1
    ); // Output: leaf(strong=1, weak=1)
    // Note: Branch's strong count became 0, so Node(5) was dropped.
    // Branch's weak count became 1 (due to leaf.parent). Allocation still exists.


    // Try accessing the parent again; Node(5) data should be gone.
    if leaf.parent.borrow().upgrade().is_none() {
        // upgrade() fails because branch's strong count is 0
        println!("Leaf's parent has been dropped (upgrade failed)."); // Should print
    } else {
        println!("Leaf's parent still exists?"); // Should not print
    }

    // leaf drops here, its strong count becomes 0, Node(3) is dropped.
    // Its weak count becomes 0, allocation is freed.
    // When Node(3) drops, its RefCell<Weak<Node>> drops. The Weak pointer to branch drops.
    // Branch's weak count becomes 0, its allocation is freed.
}

By using Weak<Node> for the parent field, the reference cycle is broken, allowing both branch and leaf nodes (and their allocations) to be deallocated correctly when their strong counts reach zero.