Smart Pointers

Reference Cycles

A reference cycle occurs when two or more reference-counted allocations (Rc<T> or Arc<T>) hold strong references to one another through interior mutability (RefCell<T>, Mutex<T>, etc.), forming a closed loop. Because each node in the cycle keeps the other node's strong_count at 1 or higher, dropping all external stack handles leaves the cycle's strong_count above zero, leaking the heap memory and skipping Drop destructors.

How a Reference Cycle Leaks Memory

Memory leaks caused by reference cycles are memory-safe (they never cause dangling pointers or undefined behavior), which is why Rust allows Rc<RefCell<T>> in safe code. However, any resources held inside the cycle remain allocated for the rest of the process lifetime.

Stack Handles Dropped:
   (a dropped)                          (b dropped)

Heap Cycle Remains (strong_count == 1 on both nodes forever):
+-----------------------+            +-----------------------+
| Node A                |   next     | Node B                |
| strong_count = 1      +----------> | strong_count = 1      |
|                       | <----------+                       |
+-----------------------+   next     +-----------------------+
use std::cell::RefCell;
use std::rc::Rc;

struct CyclicNode {
    label: &'static str,
    next: RefCell<Option<Rc<CyclicNode>>>,
}

impl Drop for CyclicNode {
    fn drop(&mut self) {
        println!("Dropping CyclicNode {}", self.label);
    }
}

fn main() {
    let a = Rc::new(CyclicNode {
        label: "A",
        next: RefCell::new(None),
    });
    let b = Rc::new(CyclicNode {
        label: "B",
        next: RefCell::new(Some(Rc::clone(&a))),
    });

    // Close the cycle: A -> B and B -> A
    *a.next.borrow_mut() = Some(Rc::clone(&b));

    println!("before scope exit -> a strong: {}, b strong: {}", Rc::strong_count(&a), Rc::strong_count(&b));
    // When `main` ends, `a` and `b` decrement strong_count from 2 to 1.
    // Neither node's `Drop` implementation ever runs!
}
before scope exit -> a strong: 2, b strong: 2

Notice that "Dropping CyclicNode A" and "Dropping CyclicNode B" are never printed.

Breaking Reference Cycles with Weak<T>

To prevent reference cycles in trees and graphs, distinguish ownership edges from non-owning back-edges:

  • Parent $\rightarrow$ Child: A parent owns its children; if the parent is dropped, its children should also be dropped (Vec<Rc<Node>>).
  • Child $\rightarrow$ Parent: A child does not own its parent; if a child is dropped, the parent must remain alive, and if the parent is dropped, the child's back-pointer must not keep the parent alive (RefCell<Weak<Node>>).
Parent-Child Hierarchy Using Strong Downward Edges and Weak Upward Edges:

          +-----------------------------------+
          | Parent Node                       |
          | children: Vec<Rc<Node>>           |
          +---------+---------------+---------+
                    |               ^
    Strong Rc<Node> |               | Weak<Node> (does not increment strong_count)
    (owns child)    v               |
          +---------+---------------+---------+
          | Child Node                        |
          | parent: RefCell<Weak<Node>>       |
          +-----------------------------------+
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct TreeNode {
    name: &'static str,
    parent: RefCell<Weak<TreeNode>>,
    children: RefCell<Vec<Rc<TreeNode>>>,
}

impl Drop for TreeNode {
    fn drop(&mut self) {
        println!("Dropping TreeNode {}", self.name);
    }
}

fn main() {
    let leaf = Rc::new(TreeNode {
        name: "leaf",
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    {
        let branch = Rc::new(TreeNode {
            name: "branch",
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "inside scope -> branch strong: {}, weak: {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch)
        );
        println!(
            "leaf parent name: {:?}",
            leaf.parent.borrow().upgrade().map(|p| p.name)
        );
    } // `branch` drops here because its strong_count is 1!

    println!(
        "after branch scope -> leaf parent: {:?}",
        leaf.parent.borrow().upgrade().map(|p| p.name)
    );
}
inside scope -> branch strong: 1, weak: 1
leaf parent name: Some("branch")
Dropping TreeNode branch
after branch scope -> leaf parent: None
Dropping TreeNode leaf

Alternative for Arbitrary Cyclic Graphs: Arena / Index Graphs

When a graph has arbitrary peer-to-peer cycles with no natural parent-child hierarchy (such as a state machine, control-flow graph, or social network), storing nodes in a contiguous Vec<Node> (or generational arena) and referencing neighbors by integer index (usize) avoids reference counting altogether and frees the entire graph when the Vec<Node> drops.