Box, Rc and RefCell

Rust · Practical · 15 min

Recursive types, shared graphs and trees, and trait objects all need smart pointers beyond plain references.

A smart pointer is a struct that owns data and acts like a pointer by implementing Deref (so *p and method calls reach the inner value) and Drop (cleanup when it goes away). String and Vec are smart pointers too.

`Box<T>` stores a T on the heap with a single owner. Use it for:

  • recursive types, whose size would otherwise be infinite: enum List { Cons(i32, Box<List>), Nil },
  • trait objects: Box<dyn Error>, Box<dyn Fn()>,
  • moving a large value around cheaply by moving only the pointer.

`Rc<T>` (reference counted) allows multiple owners of the same value in single-threaded code. Rc::clone(&rc) increments a counter instead of copying the data, and the value is dropped when the last Rc goes away. Rc::strong_count shows the count. Data inside an Rc is read-only, because shared ownership implies shared references.

`RefCell<T>` moves the borrowing rules from compile time to runtime. cell.borrow() returns a guard acting like &T, cell.borrow_mut() one acting like &mut T, and violating "many readers or one writer" panics instead of failing to compile. try_borrow_mut returns a Result instead.

Combined, `Rc<RefCell<T>>` gives shared, mutable data: a common shape for graphs, observers and GUI state. Use it sparingly; it is a sign that ownership is genuinely shared, not a way around thinking about ownership.

Two reference-counted values pointing at each other never reach a count of zero, which leaks memory. Break cycles with `Weak<T>` (Rc::downgrade), a non-owning pointer you upgrade() to an Option<Rc<T>>, typically for parent links in trees.

Rc and RefCell are not thread-safe; their multithreaded counterparts are Arc and Mutex.

Example

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

enum List { Cons(i32, Box<List>), Nil } // Box gives the recursive type a known size
fn sum(list: &List) -> i32 {
    match list { List::Cons(v, rest) => v + sum(rest), List::Nil => 0 }
}

#[derive(Default)]
struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,      // non-owning back link
    children: RefCell<Vec<Rc<Node>>>, // owning links
}

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    println!("sum = {}", sum(&list)); // sum = 3
    let root = Rc::new(Node { name: "root".into(), ..Default::default() });
    let leaf = Rc::new(Node { name: "leaf".into(), ..Default::default() });
    root.children.borrow_mut().push(Rc::clone(&leaf));
    *leaf.parent.borrow_mut() = Rc::downgrade(&root);
    let parent = leaf.parent.borrow().upgrade().map(|p| p.name.clone());
    println!("{parent:?}, leaf strong = {}", Rc::strong_count(&leaf)); // Some("root"), leaf strong = 2
}

Remember

  • Box<T>: single owner on the heap, for recursive types and trait objects
  • Rc<T>: shared ownership via reference counting, read-only, single thread
  • RefCell<T>: borrow rules checked at runtime (panics on violation); break Rc cycles with Weak

Watch out for

  • Holding a borrow() guard while calling code that does borrow_mut() on the same cell, which panics.

Check yourself

  1. What happens if you call `cell.borrow_mut()` while a `cell.borrow()` guard is still alive?
  2. Two `Rc` nodes point to each other. What happens when all outside handles are dropped?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch15-00-smart-pointers.html