Skip to content

Rust Smart Pointers

The ownership and borrowing rules cover most situations, but a few real cases don’t fit neatly into “exactly one owner” — a value whose size isn’t known until runtime, data that actually needs more than one owner, or a value you need to mutate through a shared reference. Smart pointers are Rust’s answer to each of those.

By default, values live on the stack, and the compiler needs to know their size upfront. Box<T> moves a value to the heap and gives you a fixed-size pointer to it — useful when a type’s size can’t be known at compile time, most commonly with recursive types:

fn main() {
let boxed = Box::new(5);
println!("{}", *boxed); // * to get the value out
}
▶ Try it Yourself

A classic example of where you need Box: a type that contains itself, like a linked list node:

enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list {
List::Cons(value, _) => println!("First value: {value}"),
List::Nil => println!("Empty"),
}
}

Output:

First value: 1
▶ Try it Yourself

Without Box, Cons(i32, List) would need infinite space to store — a List containing a List containing a List, forever. Box<List> is a fixed-size pointer instead, so the compiler can work out how much space it needs.

Ownership says a value has exactly one owner. Most of the time that’s exactly right — but sometimes you really do need multiple parts of your program to share the same data. Rc<T> (“reference counted”) allows that, by keeping track of how many owners currently exist:

use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("shared data"));
let b = Rc::clone(&a); // not a deep copy — just another owner
let c = Rc::clone(&a);
println!("{a} {b} {c}");
println!("Owners: {}", Rc::strong_count(&a));
}

Output:

shared data shared data shared data
Owners: 3
▶ Try it Yourself

Rc::clone doesn’t copy the underlying data — it just increments a counter and hands back another reference to the same value. The data only gets cleaned up once every owner has gone out of scope.

RefCell<T>: mutating through a shared reference

Section titled “RefCell<T>: mutating through a shared reference”

Normally, the borrowing rules are checked at compile time: either many read-only references, or one mutable one, never both. RefCell<T> moves that same check to runtime instead, which lets you mutate a value even through what looks like a shared, non-mut reference:

use std::cell::RefCell;
fn main() {
let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());
}

Output:

6
▶ Try it Yourself

RefCell is most often paired with RcRc<RefCell<T>> gives you a value with multiple owners that can still be mutated, which plain Rc<T> alone doesn’t allow.

Don’t start with smart pointers — start with plain ownership and borrowing, and reach for these only when the compiler is actively telling you it won’t fit: a recursive type (Box), a value that actually needs multiple owners (Rc), or mutation through a shared reference that borrowing rules won’t allow (RefCell).

  • Box<T> stores a value on the heap with a fixed-size pointer — needed for recursive types.
  • Rc<T> allows multiple owners of the same value in single-threaded code, via reference counting; use Arc<T> across threads.
  • RefCell<T> checks borrowing rules at runtime instead of compile time, allowing mutation through a shared reference — but panics if the rules are broken.
  • Reach for these only when plain ownership and borrowing don’t fit your situation.

Quick check

1. Why does a recursive type like a linked list node need Box<T>?

2. What does Rc::clone(&a) actually do?

3. When does RefCell<T> check the borrowing rules?

4. What does Rc<RefCell<T>> give you that plain Rc<T> alone doesn't?

Score: 0 / 4