Smart Pointers: Box, Rc & RefCell
Prerequisites: Pattern matching in depth
A smart pointer is a struct that acts like a reference but also owns or
manages the data it points to. String and Vec<T> are smart pointers
already. This lesson covers the three you will meet most often:
| Type | What it gives you | Checked |
|---|---|---|
Box<T> |
a single owner, data stored on the heap | compile time |
Rc<T> |
several owners of the same read-only data (reference counted) | compile time |
RefCell<T> |
mutation through a shared reference ("interior mutability") | runtime |
All three implement Deref, so you can call methods of the inner value
directly (boxed.len()), and Drop, so the data is freed automatically.
Box: heap allocation and recursive types
Box::new(value) moves the value to the heap and keeps a pointer on the
stack. The main uses are:
- Recursive types, whose size would otherwise be infinite. A list node that contains another list node must hold it behind a pointer.
- Trait objects such as
Box<dyn Error>orBox<dyn Fn()>. - Moving a large value around without copying its bytes.
Rc: shared ownership
Rc<T> ("reference counted") lets several parts of a program own the same
value. Rc::clone(&rc) doesn't copy the data; it increments a counter. The
value is dropped when the last Rc goes away. Rc gives only shared (&)
access, and it is for single-threaded code only — the thread-safe version,
Arc, comes in a later lesson.
RefCell: interior mutability
RefCell<T> moves the borrowing rules from compile time to runtime.
.borrow() returns a shared borrow and .borrow_mut() a mutable one; if you
break the rules (for example two borrow_mut calls alive at once), the
program panics instead of failing to compile. Combined as Rc<RefCell<T>>,
it gives several owners that can all modify the same value.
Syntax
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let b = Box::new(5); // an i32 on the heap
let shared = Rc::new(String::from("hi"));
let other = Rc::clone(&shared); // two owners, one String
let cell = RefCell::new(vec![1]);
cell.borrow_mut().push(2); // mutate through a shared reference
println!("{} {} {} {:?}", b, other, Rc::strong_count(&shared), cell.borrow());
}
Example
use std::cell::RefCell;
use std::rc::Rc;
// A recursive type needs Box: List contains another List.
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn sum(list: &List) -> i32 {
match list {
Cons(value, rest) => value + sum(rest),
Nil => 0,
}
}
#[derive(Debug)]
struct Account {
owner: String,
balance: i64,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{list:?}");
println!("sum = {}", sum(&list));
// Rc<RefCell<T>>: two owners that can both modify the same account.
let account = Rc::new(RefCell::new(Account { owner: String::from("Ada"), balance: 100 }));
let alice_view = Rc::clone(&account);
let bank_view = Rc::clone(&account);
println!("owners: {}", Rc::strong_count(&account));
alice_view.borrow_mut().balance -= 30;
bank_view.borrow_mut().balance += 5;
println!("{} has {}", account.borrow().owner, account.borrow().balance);
drop(alice_view);
println!("owners after drop: {}", Rc::strong_count(&account));
// try_borrow_mut reports a conflict instead of panicking.
let reading = account.borrow();
match account.try_borrow_mut() {
Ok(_) => println!("got a mutable borrow"),
Err(_) => println!("already borrowed: cannot mutate while reading"),
}
drop(reading);
println!("free again: {}", account.try_borrow_mut().is_ok());
}
Expected output
Cons(1, Cons(2, Cons(3, Nil)))
sum = 6
owners: 3
Ada has 75
owners after drop: 2
already borrowed: cannot mutate while reading
free again: true
Weak: breaking reference cycles
If two values hold Rcs to each other, their counts never reach zero and the
memory leaks. Weak<T>, created with Rc::downgrade(&rc), is a pointer that
doesn't keep the value alive. Call .upgrade() to get an Option<Rc<T>>:
Some if the value still exists, None if it has been dropped. A typical use
is a tree where parents own children with Rc and children point back to
their parent with Weak.
use std::rc::{Rc, Weak};
fn main() {
let strong = Rc::new(String::from("parent"));
let weak: Weak<String> = Rc::downgrade(&strong);
println!("{:?}", weak.upgrade()); // Some("parent")
drop(strong);
println!("{:?}", weak.upgrade()); // None
}
Common mistakes
- Defining a recursive type without indirection:
enum List { Cons(i32, List), Nil }fails witherror[E0072]: recursive type `List` has infinite size. Wrap the inner value inBox. - Holding a
borrow()while callingborrow_mut()on the sameRefCell. It compiles, then panics at runtime (Rust 1.85 printsalready borrowed: BorrowMutError; newer versions sayRefCell already borrowed). Keep borrows short, or usetry_borrow_mut. - Trying to mutate through a plain
Rc<T>:error[E0594]: cannot assign to data in an `Rc`. Put aRefCellinside. - Writing
shared.clone()and wondering whether it deep-copies. On anRcit only bumps the count, butRc::clone(&shared)makes that obvious to readers. - Reaching for
Rc<RefCell<T>>by default. Plain ownership and borrowing are simpler and checked at compile time; use these types when shared ownership is genuinely needed.
Practice
Create an Rc<RefCell<Vec<String>>> log shared by two "components"
(two Rc clones). Push one message through each clone, then print the
number of messages, the messages joined by " | ", and the strong count.
Show solution
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let network = Rc::clone(&log);
let storage = Rc::clone(&log);
network.borrow_mut().push(String::from("connected"));
storage.borrow_mut().push(String::from("saved file"));
let entries = log.borrow();
println!("{} messages", entries.len());
println!("{}", entries.join(" | "));
println!("strong count: {}", Rc::strong_count(&log));
}
2 messages
connected | saved file
strong count: 3