Smart Pointers: Box, Rc & RefCell
Ownership says every value has exactly one owner, and borrowing says you may change a value only through one &mut at a time. These rules cover most programs. Some designs need more: a type that contains itself (a tree), a value with several owners (a playlist shared by two users), or data that must change while it is shared.
For these cases Rust has smart pointers: structs that act like references but also own their data and add an extra ability. You have already used two, String and Vec, which own heap memory. In this lesson you will learn Box, Rc, RefCell and Weak, and when each one is the right tool.
Box: a value on the heap
Box<T> stores a value on the heap and keeps only a pointer on the stack. It has a single owner, and when the box is dropped, so is the value. You use a box just like the value inside, thanks to the Deref trait:
enum Expr {
Num(i64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
fn eval(e: &Expr) -> i64 {
match e {
Expr::Num(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Mul(a, b) => eval(a) * eval(b),
}
}
fn main() {
let price = Box::new(1299);
println!("Price on the heap: {price}");
println!("With GST: {}", *price * 118 / 100);
// (2 + 3) * 4
let expr = Expr::Mul(
Box::new(Expr::Add(Box::new(Expr::Num(2)), Box::new(Expr::Num(3)))),
Box::new(Expr::Num(4)),
);
println!("(2 + 3) * 4 = {}", eval(&expr));
}Price on the heap: 1299 With GST: 1532 (2 + 3) * 4 = 20
Boxing a single number, as with price, is only a demonstration. The real use is the Expr enum. An Expr can contain other Expr values, so without the boxes its size would be infinite: an Add holds an Expr, which might hold an Add, and so on. The compiler rejects that with error E0072, "recursive type Expr has infinite size", and suggests adding indirection such as a Box. A box always has the size of one pointer, so the enum gets a fixed size.
Box has three common uses:
- Recursive types such as trees, expression trees and linked lists.
- Trait objects, such as
Box<dyn Fare>from the Traits lesson. - Large values that you want to move around cheaply: moving a box copies only the pointer.
Rc: shared ownership
Sometimes a value really has several owners, and none of them knows which will finish last. Rc<T> (reference counted) keeps a count of owners. Rc::clone does not copy the data; it adds one to the count and returns another pointer to the same value. When the last Rc is dropped, the count reaches zero and the value is freed.
use std::rc::Rc;
struct Playlist {
name: String,
songs: Vec<String>,
}
fn main() {
let road_trip = Rc::new(Playlist {
name: String::from("Road trip"),
songs: vec![String::from("Track 1"), String::from("Track 2")],
});
println!("Owners: {}", Rc::strong_count(&road_trip));
let priya_library = Rc::clone(&road_trip);
let rohan_library = Rc::clone(&road_trip);
println!("Owners: {}", Rc::strong_count(&road_trip));
println!("Priya sees {} songs", priya_library.songs.len());
drop(rohan_library);
println!("Owners: {}", Rc::strong_count(&road_trip));
println!("Still here: {}", road_trip.name);
}Owners: 1 Owners: 3 Priya sees 2 songs Owners: 2 Still here: Road trip
Two limits matter. An Rc gives only shared access, so you cannot change the playlist through it. And Rc is for one thread only: its counter is not safe to update from several threads at once, and the compiler stops you from sending it to another thread. The thread-safe version, Arc, is covered in Fearless Concurrency.
RefCell: borrow checking at run time
The normal borrowing rules are checked at compile time. Occasionally you know a program is correct but the compiler cannot prove it, typically when shared data needs to change. RefCell<T> moves the borrow check to run time: borrow() gives a shared borrow, borrow_mut() gives a mutable borrow, and RefCell counts them while the program runs. This is called interior mutability, because you change the inside of a value that is not declared mut.
use std::cell::RefCell;
fn main() {
let cart = RefCell::new(vec![String::from("rice")]);
cart.borrow_mut().push(String::from("dal"));
println!("Items: {:?}", cart.borrow());
let reading = cart.borrow();
println!("Can change now? {}", cart.try_borrow_mut().is_ok());
drop(reading);
println!("Can change now? {}", cart.try_borrow_mut().is_ok());
}Items: ["rice", "dal"] Can change now? false Can change now? true
The rules are the same as always (many readers or one writer), but a violation is no longer a compile error. If you call borrow_mut() while a borrow() is still alive, the program panics. Here we used try_borrow_mut, which returns an error instead of panicking, to show the check. Keep RefCell borrows short, ideally within one statement, as on the push line. For simple Copy values such as a counter, Cell<T> with get and set is lighter still.
Rc and RefCell together
Rc<RefCell<T>> is the classic combination for single-threaded data that has several owners and must change: Rc provides the sharing, RefCell provides the mutability.
use std::cell::RefCell;
use std::rc::Rc;
struct Account {
owner: String,
balance: u32,
}
fn main() {
let joint = Rc::new(RefCell::new(Account {
owner: String::from("Sharma family"),
balance: 10_000,
}));
let parent = Rc::clone(&joint);
let daughter = Rc::clone(&joint);
parent.borrow_mut().balance += 5_000;
daughter.borrow_mut().balance -= 2_500;
let account = joint.borrow();
println!("{}: Rs. {}", account.owner, account.balance);
println!("Card holders: {}", Rc::strong_count(&joint));
}Sharma family: Rs. 12500 Card holders: 3
Each borrow_mut() lasts only until the end of its statement, so the two updates never overlap and nothing panics.
Weak: breaking reference cycles
If two Rc values point at each other (a parent that owns its children, and children that own their parent), neither count can ever reach zero, and the memory leaks. Rust's safety rules do not prevent leaks. The fix is Weak<T>: a non-owning pointer created with Rc::downgrade(&rc). A weak pointer does not keep the value alive; call upgrade() to get an Option<Rc<T>>, which is None if the value is already gone. In a tree, parents hold children with Rc and children point back to parents with Weak.
| Type | Owners | Can change data? | Checked | Threads |
|---|---|---|---|---|
Box<T> | One | Yes, if the box is mut | Compile time | Can move between threads |
Rc<T> | Many | No | Compile time | One thread only |
RefCell<T> | One | Yes, through borrow_mut() | Run time (panics) | Can move, but not be shared, between threads |
Rc<RefCell<T>> | Many | Yes | Run time | One thread only |
Weak<T> | None (observes an Rc) | No | upgrade() may give None | One thread only |
Common mistakes
- Reaching for
Rc<RefCell<T>>first. It moves errors to run time. Try plain ownership and borrowing first, thenBox, and use shared mutable state only when the design truly needs it. - Holding a
borrow()while callingborrow_mut(). This panics at run time. Keep borrows short or usetry_borrow_mut. - Creating
Rccycles. They leak memory. UseWeakfor back-references. - Trying to share
RcorRefCellbetween threads. The compiler refuses; useArcandMutexinstead.
Key points
- Smart pointers own their data and add abilities;
StringandVecare smart pointers too. Box<T>puts a value on the heap with one owner; it enables recursive types and trait objects.Rc<T>allows several owners in one thread by counting references;Rc::cloneonly increases the count.RefCell<T>checks borrowing rules at run time and allows changing shared data; violations panic.Rc<RefCell<T>>gives shared, mutable data in one thread, andWeak<T>breaks reference cycles.
Exercise
Model a folder tree with Box. Create an enum Item with variants File { name: String, size_kb: u32 } and Folder { name: String, children: Vec<Item> } (a Vec already stores items on the heap, so no Box is needed here). Write a recursive function total_size(item: &Item) -> u32, and build a small tree for a "Projects" folder containing two files and a "Photos" sub-folder with one file. Print the total size.
Show solution
enum Item {
File { name: String, size_kb: u32 },
Folder { name: String, children: Vec<Item> },
}
fn total_size(item: &Item) -> u32 {
match item {
Item::File { size_kb, .. } => *size_kb,
Item::Folder { children, .. } => {
let mut sum = 0;
for child in children {
sum += total_size(child);
}
sum
}
}
}
fn main() {
let projects = Item::Folder {
name: String::from("Projects"),
children: vec![
Item::File { name: String::from("report.pdf"), size_kb: 820 },
Item::File { name: String::from("budget.xlsx"), size_kb: 64 },
Item::Folder {
name: String::from("Photos"),
children: vec![Item::File { name: String::from("team.jpg"), size_kb: 2_300 }],
},
],
};
println!("Total size: {} KB", total_size(&projects));
}Total size: 3184 KB
Vec<Item> breaks the infinite size just like Box, because the vector stores its items on the heap and keeps only a pointer, length and capacity inline. The compiler will warn that the name fields are never read; a real program would print them.
Quick Quiz
1. Why does a recursive enum like enum Expr { Num(i64), Add(Expr, Expr) } fail to compile?
Wrapping the recursive fields in Box<Expr> makes the enum's size known at compile time.
2. What does Rc::clone(&shared) do?
Rc::clone is cheap: it only increments the strong count.
3. What happens if you call borrow_mut() on a RefCell while a borrow() is still active?
RefCell enforces the borrowing rules at run time and panics on a violation.
4. Which pointer should a child node use to refer back to its parent to avoid a memory leak?
Weak does not own the value, so it does not keep a cycle of reference counts alive.
Learn Rust with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
