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
}

Output:

5
▶ 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 Rc — Rc<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).

Use the List enum from the Box section. Build the list 1, 2, 3 and write a function fn sum(list: &List) -> i32 that adds up every value. Hint: match on the list. For Cons(value, rest), the answer is value plus the sum of rest. For Nil, it’s 0.

Show solution
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, rest) => value + sum(rest),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("Sum: {}", sum(&list));
}

Output:

Sum: 6
▶ Try it Yourself

The function calls itself on the rest of the list until it reaches Nil. Notice you can pass rest (a &Box<List>) where a &List is expected. Rust looks through the Box for you.

Make one shopping list (a Vec<String>) that two people, alice and bob, both own and can both add to. Each adds one item. Then print the list and the number of owners. Hint: wrap the Vec in Rc<RefCell<...>>, give each person an Rc::clone, and call .borrow_mut().push(...) to add.

Show solution
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let list = Rc::new(RefCell::new(Vec::new()));
let alice = Rc::clone(&list);
let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("milk"));
bob.borrow_mut().push(String::from("bread"));
println!("{:?}", list.borrow());
println!("Owners: {}", Rc::strong_count(&list));
}

Output:

["milk", "bread"]
Owners: 3
▶ Try it Yourself

There are three owners: list, alice and bob. All three point at the same Vec, so an item added through alice is visible through bob and list too. None of the variables needs mut, because RefCell handles the mutation.

  • 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