Weak Pointers

Breaking cycles in graph structures

advanced
weakgraphscycles
๐ŸŽฎ Interactive Playground

What are Weak Pointers?

Weak is a non-owning reference to Rc or Arc. It doesn't increase the reference count, so it doesn't prevent the value from being dropped. The Problem: Reference Cycles
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    next: Option<Rc<RefCell<Node>>>,
}

// Create cycle: A โ†’ B โ†’ A
let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));
a.borrow_mut().next = Some(Rc::clone(&b));

// Memory leak! Both have ref count 2, neither drops
The Solution: Weak References
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    next: Option<Rc<RefCell<Node>>>,
    prev: Weak<RefCell<Node>>,  // Weak doesn't prevent drop
}

Real-World Example 1: Parent-Child Tree (Web/Systems)

use std::rc::{Rc, Weak};
use std::cell::RefCell;

/// DOM-like tree with parent-child relationships
pub struct TreeNode {
    pub value: String,
    pub parent: RefCell<Weak<TreeNode>>,      // Weak to parent
    pub children: RefCell<Vec<Rc<TreeNode>>>, // Strong to children
}

impl TreeNode {
    pub fn new(value: impl Into<String>) -> Rc<Self> {
        Rc::new(TreeNode {
            value: value.into(),
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    pub fn add_child(parent: &Rc<TreeNode>, child: Rc<TreeNode>) {
        // Set child's parent (weak reference)
        *child.parent.borrow_mut() = Rc::downgrade(parent);

        // Add child to parent's children (strong reference)
        parent.children.borrow_mut().push(child);
    }

    pub fn get_parent(&self) -> Option<Rc<TreeNode>> {
        // Upgrade weak to strong (may fail if parent was dropped)
        self.parent.borrow().upgrade()
    }

    pub fn print_path_to_root(&self) {
        print!("{}", self.value);

        if let Some(parent) = self.get_parent() {
            print!(" โ† ");
            parent.print_path_to_root();
        } else {
            println!(" (root)");
        }
    }
}

// Usage
fn tree_example() {
    let root = TreeNode::new("root");
    let child1 = TreeNode::new("child1");
    let grandchild = TreeNode::new("grandchild");

    TreeNode::add_child(&root, Rc::clone(&child1));
    TreeNode::add_child(&child1, Rc::clone(&grandchild));

    // Navigate up the tree
    grandchild.print_path_to_root();

    // Check reference counts
    println!("Root strong count: {}", Rc::strong_count(&root));       // 1
    println!("Child1 strong count: {}", Rc::strong_count(&child1));   // 2
    println!("Root weak count: {}", Rc::weak_count(&root));           // 1 (child1's parent ref)
}

Real-World Example 2: Observer Pattern (Systems)

use std::rc::{Rc, Weak};
use std::cell::RefCell;

/// Event subject that notifies observers
pub struct Subject {
    observers: RefCell<Vec<Weak<dyn Observer>>>,
    state: RefCell<i32>,
}

pub trait Observer {
    fn update(&self, state: i32);
}

impl Subject {
    pub fn new() -> Rc<Self> {
        Rc::new(Subject {
            observers: RefCell::new(Vec::new()),
            state: RefCell::new(0),
        })
    }

    /// Register observer (weak reference)
    pub fn attach(&self, observer: Weak<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }

    /// Update state and notify all observers
    pub fn set_state(&self, new_state: i32) {
        *self.state.borrow_mut() = new_state;
        self.notify();
    }

    fn notify(&self) {
        // Clean up dead observers while notifying
        self.observers.borrow_mut().retain(|weak_obs| {
            if let Some(observer) = weak_obs.upgrade() {
                observer.update(*self.state.borrow());
                true  // Keep observer
            } else {
                false  // Remove dead observer
            }
        });
    }
}

/// Concrete observer
struct ConcreteObserver {
    id: usize,
}

impl Observer for ConcreteObserver {
    fn update(&self, state: i32) {
        println!("Observer {} received update: state = {}", self.id, state);
    }
}

// Usage
fn observer_example() {
    let subject = Subject::new();

    {
        let obs1 = Rc::new(ConcreteObserver { id: 1 });
        let obs2 = Rc::new(ConcreteObserver { id: 2 });

        subject.attach(Rc::downgrade(&obs1));
        subject.attach(Rc::downgrade(&obs2));

        subject.set_state(10);  // Both observers notified

        // obs2 goes out of scope here
    }

    // obs2 dropped, only obs1's weak ref remains (but it's also out of scope)
    subject.set_state(20);  // No observers (auto-cleaned)
}

Real-World Example 3: Cache with Eviction (Web)

use std::rc::{Rc, Weak};
use std::cell::RefCell;
use std::collections::HashMap;

/// Cache entry that can be evicted
pub struct CachedValue {
    pub data: String,
    pub size: usize,
}

/// Cache that holds weak references to allow eviction
pub struct WeakCache {
    entries: RefCell<HashMap<String, Weak<CachedValue>>>,
    total_size: RefCell<usize>,
    max_size: usize,
}

impl WeakCache {
    pub fn new(max_size: usize) -> Self {
        WeakCache {
            entries: RefCell::new(HashMap::new()),
            total_size: RefCell::new(0),
            max_size,
        }
    }

    /// Get value from cache
    pub fn get(&self, key: &str) -> Option<Rc<CachedValue>> {
        self.entries.borrow().get(key)?.upgrade()
    }

    /// Insert value into cache (stores weak reference)
    pub fn insert(&self, key: String, value: Rc<CachedValue>) {
        let weak = Rc::downgrade(&value);
        self.entries.borrow_mut().insert(key, weak);
        *self.total_size.borrow_mut() += value.size;
    }

    /// Cleanup dead entries
    pub fn cleanup(&self) {
        let mut entries = self.entries.borrow_mut();
        let mut total_size = self.total_size.borrow_mut();

        entries.retain(|_, weak| {
            if let Some(value) = weak.upgrade() {
                true  // Still alive
            } else {
                // Entry died, update size
                false
            }
        });

        // Recalculate total size
        *total_size = entries.values()
            .filter_map(|w| w.upgrade())
            .map(|v| v.size)
            .sum();

        println!("Cache cleanup: {} entries, {} bytes", entries.len(), *total_size);
    }
}

// Usage
fn cache_example() {
    let cache = WeakCache::new(1000);

    {
        let value1 = Rc::new(CachedValue {
            data: "data1".to_string(),
            size: 100,
        });
        cache.insert("key1".to_string(), Rc::clone(&value1));

        // value1 in scope, cache hit
        assert!(cache.get("key1").is_some());
    }

    // value1 dropped, weak reference becomes invalid
    assert!(cache.get("key1").is_none());

    cache.cleanup();  // Removes dead entry
}

Weak::upgrade() - Converting Weak to Strong

use std::rc::{Rc, Weak};

let strong = Rc::new(42);
let weak = Rc::downgrade(&strong);

// Try to upgrade weak to strong
match weak.upgrade() {
    Some(value) => println!("Value still alive: {}", value),
    None => println!("Value was dropped"),
}

drop(strong);  // Drop the last strong reference

// Now upgrade fails
assert!(weak.upgrade().is_none());
upgrade() is O(1) - just checks counter and increments if non-zero

Reference Counting with Weak

let strong = Rc::new(42);
let weak1 = Rc::downgrade(&strong);
let weak2 = Rc::downgrade(&strong);

println!("Strong count: {}", Rc::strong_count(&strong));  // 1
println!("Weak count: {}", Rc::weak_count(&strong));      // 2

// Value dropped when strong count reaches 0
// Weak references just become invalid

Thread-Safe Version: Arc + Weak

use std::sync::{Arc, Weak};
use std::thread;

let data = Arc::new(vec![1, 2, 3]);
let weak = Arc::downgrade(&data);

thread::spawn(move || {
    if let Some(data) = weak.upgrade() {
        println!("Data still alive: {:?}", data);
    } else {
        println!("Data was dropped");
    }
}).join().unwrap();

Advanced Pattern: Self-Referential Graph

use std::rc::{Rc, Weak};
use std::cell::RefCell;

pub struct Graph {
    nodes: Vec<Rc<RefCell<GraphNode>>>,
}

pub struct GraphNode {
    pub id: usize,
    pub neighbors: Vec<Weak<RefCell<GraphNode>>>,  // Weak to prevent cycles
}

impl Graph {
    pub fn new() -> Self {
        Graph { nodes: Vec::new() }
    }

    pub fn add_node(&mut self, id: usize) -> Rc<RefCell<GraphNode>> {
        let node = Rc::new(RefCell::new(GraphNode {
            id,
            neighbors: Vec::new(),
        }));
        self.nodes.push(Rc::clone(&node));
        node
    }

    pub fn add_edge(
        &self,
        from: &Rc<RefCell<GraphNode>>,
        to: &Rc<RefCell<GraphNode>>,
    ) {
        from.borrow_mut().neighbors.push(Rc::downgrade(to));
    }

    pub fn traverse(&self, start_id: usize) {
        if let Some(node) = self.nodes.iter().find(|n| n.borrow().id == start_id) {
            self.visit(node, &mut std::collections::HashSet::new());
        }
    }

    fn visit(&self, node: &Rc<RefCell<GraphNode>>, visited: &mut std::collections::HashSet<usize>) {
        let node_ref = node.borrow();

        if visited.contains(&node_ref.id) {
            return;
        }

        println!("Visiting node {}", node_ref.id);
        visited.insert(node_ref.id);

        for weak_neighbor in &node_ref.neighbors {
            if let Some(neighbor) = weak_neighbor.upgrade() {
                drop(node_ref);  // Release borrow before recursion
                self.visit(&neighbor, visited);
            }
        }
    }
}

Memory Layout

Strong Rc:
Stack:              Heap:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚Rc<T>โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  โ”‚strong: 2     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚weak: 1       โ”‚
                   โ”‚value: T      โ”‚
                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Weak Rc:
Stack:              (same heap)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚Weak<T>โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚strong: 2     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚weak: 1       โ”‚
                   โ”‚value: T      โ”‚
                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Key Points:
  • Weak doesn't increment strong_count
  • Weak DOES increment weak_count
  • Allocation stays alive while weak_count > 0 (for bookkeeping)
  • Value dropped when strong_count == 0

โš ๏ธ Anti-Patterns and Common Mistakes

โš ๏ธ โŒ Mistake #1: Not Handling upgrade() Failure

// BAD: Unwrapping upgrade without checking
let weak: Weak<Data> = ...;
let strong = weak.upgrade().unwrap();  // PANIC if value dropped!

// GOOD: Handle None case
if let Some(strong) = weak.upgrade() {
    // Use strong
} else {
    // Handle case where value is gone
}

โš ๏ธ โŒ Mistake #2: Using Strong References in Cycles

// BAD: Creates cycle
struct Node {
    parent: Option<Rc<Node>>,  // Strong reference
    children: Vec<Rc<Node>>,   // Strong references
}
// Parent keeps children alive, children keep parent alive = LEAK

// GOOD: Break cycle with Weak
struct Node {
    parent: Weak<Node>,        // Weak to parent
    children: Vec<Rc<Node>>,   // Strong to children
}

โš ๏ธ โŒ Mistake #3: Forgetting to Clean Up Dead Weak References

// BAD: Accumulates dead weak references
struct Subject {
    observers: Vec<Weak<Observer>>,
}

impl Subject {
    fn notify(&self) {
        for weak in &self.observers {
            if let Some(obs) = weak.upgrade() {
                obs.update();
            }
            // Dead observers accumulate!
        }
    }
}

// GOOD: Clean up during iteration
fn notify(&mut self) {
    self.observers.retain(|weak| {
        if let Some(obs) = weak.upgrade() {
            obs.update();
            true  // Keep
        } else {
            false  // Remove dead observer
        }
    });
}

When to Use Weak Pointers

โœ… Use Weak When:

  1. Parent-Child Trees: Child needs reference to parent
  2. Observer Pattern: Subjects shouldn't keep observers alive
  3. Caches: Allow cached values to be evicted
  4. Cyclic Graphs: Break cycles in graph structures
  5. Backpointers: Secondary references that shouldn't own

โŒ Avoid Weak When:

  1. Ownership Needed: Use Rc/Arc if reference should keep value alive
  2. Simple Trees: If tree is always top-down, Weak not needed
  3. Performance Critical: upgrade() has overhead
  4. No Cycles: If no cycles possible, use strong references

Performance Characteristics

| Operation | Cost | Notes |

|-----------|------|-------|

| Downgrade | ~2 cycles | Increment weak_count |

| Upgrade | ~5-10 cycles | Check + increment strong_count |

| Drop Weak | ~2 cycles | Decrement weak_count |

| Size | 8 bytes | One pointer |

Exercises

Exercise 1: Doubly-Linked List

Implement a doubly-linked list using Rc + Weak.

Hints:
  • next: Option>
  • prev: Weak
  • Break cycles with weak prev pointers

Exercise 2: Event Bus

Build an event bus where subscribers use weak references.

Hints:
  • subscribers: Vec>
  • Auto-cleanup dead subscribers
  • Prevent subscriber lifetime issues

Exercise 3: Undo/Redo Stack

Implement undo/redo with weak references to commands.

Hints:
  • Commands may be evicted from memory
  • Weak allows checking if command still valid
  • Handle upgrade() failures gracefully

Further Reading

Real-World Usage

๐Ÿฆ€ GTK-rs

Uses Weak for widget parent references to prevent cycles.

View on GitHub

๐Ÿฆ€ Servo

Browser engine uses Weak for DOM parent references.

View on GitHub

๐Ÿฆ€ Tokio

Uses Weak for task waker references.

View on GitHub

๐ŸŽฎ Try it Yourself

๐ŸŽฎ

Weak Pointers - Playground

Run this code in the official Rust Playground