← thecodex.expert · The Codex Family of Knowledge
Rust

Smart Pointers

When ownership does not fit the usual one-owner rule, Rust offers Box, Rc, and RefCell to relax it in specific, controlled ways.

Rust 1.95+ Box, Rc, RefCell Last verified:
Where this bites you

Rc frees its data the moment its strong count hits zero -- but nothing forces that count to ever reach zero. Two Rcs that end up pointing back at each other, each counted as one of the other's owners, keep each other's count at one forever. Nobody panics, nothing errors: the memory is simply never freed, for as long as the program runs.

Rustcycle_bug.rs
struct Node {
    name: String,
    next: RefCell<Option<Rc<Node>>>,
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("dropping node {}", self.name);   // should print for every node
    }
}

let a = Rc::new(Node { name: "a".to_string(), next: RefCell::new(None) });
let b = Rc::new(Node { name: "b".to_string(), next: RefCell::new(None) });

*a.next.borrow_mut() = Some(Rc::clone(&b));   // a points to b
*b.next.borrow_mut() = Some(Rc::clone(&a));   // b points back to a -- a cycle

println!("a strong_count = {}", Rc::strong_count(&a));   // 2
drop(a);
drop(b);
// neither "dropping node" line ever prints -- the cycle keeps both alive forever
Canonical Definition

A smart pointer is a data structure that acts like a pointer but adds extra capabilities. Box<T> puts a value on the heap with ordinary single ownership, useful for recursive types or large data you don't want to copy. Rc<T> (reference counted) allows multiple owners of the same data by tracking a count of active references, freeing the data when the count hits zero — single-threaded only; the thread-safe equivalent is Arc<T>. RefCell<T> enforces the borrowing rules at runtime instead of compile time, allowing mutation through a reference that looks immutable, at the cost of a possible runtime panic instead of a compile error if the rules are violated.

Box<T>: heap allocation, single owner

Box is needed for recursive types, since Rust must know a type's size at compile time — a type that contains itself directly would be infinitely large, but a Box (a fixed-size pointer) breaks the cycle.

Rustbox_basics.rs
enum List {
    Cons(i32, Box<List>),   // Box breaks the infinite-size cycle
    Nil,
}

use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

Rc<T>: multiple owners via reference counting

Rc::clone doesn't deep-copy the data — it increments a reference count and returns a new pointer to the same heap allocation. The data is freed only when the count reaches zero.

Rustrc_basics.rs
use std::rc::Rc;

let a = Rc::new(String::from("shared data"));
println!("{}", Rc::strong_count(&a));   // 1

let b = Rc::clone(&a);   // increments the count, doesn't copy the string
println!("{}", Rc::strong_count(&a));   // 2

drop(b);
println!("{}", Rc::strong_count(&a));   // 1 again

That count is exactly what leaks in the bug above: each node's next holds a strong Rc to the other, so dropping your own two named bindings only brings each count from 2 down to 1 -- never to 0 -- and the Drop that would print "dropping node..." never runs. Weak<T> is the fix: a Weak reference doesn't count toward whether the data stays alive, so a back-reference built with Rc::downgrade instead of Rc::clone can't create a cycle that keeps itself alive:

Rustcycle_fix.rs
struct Node {
    name: String,
    next: RefCell<Option<Weak<Node>>>,   // a weak back-reference breaks the cycle
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("dropping node {}", self.name);
    }
}

let a = Rc::new(Node { name: "a".to_string(), next: RefCell::new(None) });
let b = Rc::new(Node { name: "b".to_string(), next: RefCell::new(None) });

*a.next.borrow_mut() = Some(Rc::downgrade(&b));
*b.next.borrow_mut() = Some(Rc::downgrade(&a));

println!("a strong_count = {}", Rc::strong_count(&a));   // 1
drop(a);
drop(b);
// dropping node a
// dropping node b

RefCell<T>: interior mutability, checked at runtime

RefCell lets you mutate data even through what the type system sees as an immutable reference, by moving the borrow-checking rules to runtime — .borrow_mut() panics if another borrow is already active, exactly the rule the compiler would otherwise enforce statically.

Rustrefcell_basics.rs
use std::cell::RefCell;

let value = RefCell::new(5);

*value.borrow_mut() += 1;   // mutate through a "shared" RefCell
println!("{}", value.borrow());   // 6

// let a = value.borrow_mut();
// let b = value.borrow_mut();   // RUNTIME PANIC: already mutably borrowed

Sources

1
The Rust Project. The Rust Programming Language, "Smart Pointers", doc.rust-lang.org/book/ch15.