Smart Pointers (Box, Rc, RefCell)
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and smart pointers give you heap allocation, shared ownership, and interior mutability while keeping those guarantees.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll meet Box for heap data and recursion, Rc for shared ownership, and RefCell for interior mutability.
What You'll Learn in This Lesson
- What a smart pointer is
- Box<T> for heap data and recursive types
- Rc<T> for multiple owners of one value
- Counting owners with strong_count
- RefCell<T> for interior mutability
- The Rc<RefCell<T>> combo
1️⃣ Box: Heap Allocation and Recursion
Box<T> is the simplest smart pointer: it puts a value on the heap and owns it. You access the value just like a reference thanks to auto-dereferencing. Its standout use is enabling recursive types — without the indirection a Box provides, a type that contains itself would have infinite size.
// Box<T> stores a value on the HEAP behind a pointer. Two main uses:
// 1. Put a large value on the heap.
// 2. Enable RECURSIVE types whose size would otherwise be infinite.
fn main() {
// A simple heap-allocated integer.
let boxed = Box::new(5);
println!("boxed value: {}", boxed); // -> boxed value: 5 (auto-deref)
// A recursive list needs Box, or its size couldn't be known at compile time.
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:?}", list);
// -> Cons(1, Cons(2, Cons(3, Nil)))
}The Cons list could only be defined because each link's tail is a Box<List> — a fixed-size pointer — rather than another List inline.
2️⃣ Rc and RefCell: Sharing and Interior Mutability
Rc<T> (reference counted) lets several owners share the same data; cloning bumps a count rather than copying. RefCell<T> moves borrow checking to runtime, letting you mutate data through a shared reference. Combined as Rc<RefCell<T>>, you get shared and mutable data.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Rc<T> = Reference Counted: allows MULTIPLE owners of the same data.
// Cloning an Rc just bumps the count; it doesn't copy the data.
let shared = Rc::new(String::from("shared data"));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
println!("value: {}", shared);
println!("owners: {}", Rc::strong_count(&shared)); // -> owners: 3
// RefCell<T> allows INTERIOR MUTABILITY: mutate data through a shared
// reference, with the borrow rules checked at RUNTIME instead of compile time.
let counter = RefCell::new(0);
*counter.borrow_mut() += 1; // mutably borrow and change
*counter.borrow_mut() += 1;
println!("counter: {}", counter.borrow()); // -> counter: 2
// Rc<RefCell<T>> is the classic combo: shared ownership + mutability.
let log = Rc::new(RefCell::new(Vec::new()));
let log2 = Rc::clone(&log);
log.borrow_mut().push("first");
log2.borrow_mut().push("second"); // both handles see the change
println!("log: {:?}", log.borrow()); // -> log: ["first", "second"]
}Both log and log2 pointed at the same vector, so a push through either was visible through both. That's the power — and the responsibility — of shared mutable state.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the smart-pointer code, then run it.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// 1) Put the number 42 on the heap with a Box.
let boxed = ___::new(42); // 👉 the heap-allocating smart pointer
println!("boxed: {}", boxed); // expected: boxed: 42
// 2) Share a value with Rc and count the owners.
let shared = Rc::new(String::from("hi"));
let _clone = Rc::___(&shared); // 👉 the method that adds an owner
println!("owners: {}", Rc::strong_count(&shared)); // expected: owners: 2
// 3) Mutate through a RefCell.
let cell = RefCell::new(10);
*cell.borrow_mut() += 5;
println!("cell: {}", cell.borrow()); // expected: cell: 15
}
// ✅ Expected output:
// boxed: 42
// owners: 2
// cell: 15Mini-Challenge: Shared Mutable Counter
Combine Rc and RefCell to share and mutate a counter through two handles. Run it with cargo run.
// 🎯 MINI-CHALLENGE: a shared, mutable counter.
//
// 1. use std::rc::Rc; and use std::cell::RefCell;
// 2. Create let counter = Rc::new(RefCell::new(0));
// 3. Make a second handle with Rc::clone.
// 4. Increment through BOTH handles: *handle.borrow_mut() += 1; (do it twice
// each, for a total of 4).
// 5. Print the final value (should be 4) and the owner count (should be 2).
//
// ✅ Example output:
// Count: 4
// Owners: 2
fn main() {
// your code here
}Common Errors (and the fix)
- "recursive type has infinite size" — Wrap the recursive field in a Box to give it a known size.
- "already borrowed: BorrowMutError" — A runtime panic from RefCell: you held two mutable borrows at once. Scope each borrow so they don't overlap.
- "`Rc<T>` cannot be sent between threads safely" — Rc is single-threaded. Use Arc across threads (next lesson).
- Forgetting the imports — Add use std::rc::Rc; and use std::cell::RefCell;.
Pro Tips
- 💡 Use Box::clone sparingly: reach for smart pointers when ownership truly needs to be shared or heap-bound, not by default.
- 💡 Keep RefCell borrows short: finish a borrow_mut() before taking another to avoid runtime panics.
- 💡 Rc<RefCell<T>> is the go-to: it's the standard pattern for shared, mutable, single-threaded state.
📋 Quick Reference — Smart Pointers
| Type | Purpose |
|---|---|
| Box::new(x) | Heap-allocate, single owner |
| Rc::new(x) | Shared ownership (count) |
| Rc::clone(&r) | Add another owner |
| RefCell::new(x) | Interior mutability |
| cell.borrow_mut() | Mutable runtime borrow |
| Rc<RefCell<T>> | Shared + mutable |
🎉 Lesson Complete!
- ✅ Smart pointers act like references but add capabilities
- ✅ Box<T> heap-allocates and enables recursive types
- ✅ Rc<T> allows multiple owners (single-threaded)
- ✅ RefCell<T> gives interior mutability with runtime borrow checks
Practice quiz
Where does Box<T> store its value?
- On the stack
- In a register
- On the heap
- In static memory
Answer: On the heap. Box<T> allocates its value on the heap behind a single owner.
Why is Box needed for a recursive type like a Cons list?
- Without indirection the type would have infinite size
- For speed
- To enable cloning
- To allow threads
Answer: Without indirection the type would have infinite size. Box gives a fixed-size pointer so the recursive type's size is known.
What does Rc stand for?
- Raw cell
- Recursive container
- Read copy
- Reference counted
Answer: Reference counted. Rc is a reference-counted smart pointer allowing multiple owners.
What does Rc::clone(&shared) do?
- Deep-copies the data
- Increments the reference count
- Moves ownership
- Frees the data
Answer: Increments the reference count. Rc::clone bumps the owner count rather than copying the data.
After two Rc::clone calls on a fresh Rc, what is the strong_count?
- 3
- 1
- 2
- 0
Answer: 3. The original plus two clones gives a strong_count of 3.
What does RefCell<T> provide?
- Thread safety
- Compile-time-only checks
- Interior mutability with runtime borrow checks
- Heap allocation
Answer: Interior mutability with runtime borrow checks. RefCell allows mutation through a shared reference, checked at runtime.
Is Rc<T> thread-safe?
- Yes
- No, it is single-threaded; use Arc across threads
- Only with RefCell
- Only on the heap
Answer: No, it is single-threaded; use Arc across threads. Rc is single-threaded; Arc is the thread-safe atomic counterpart.
What does *counter.borrow_mut() += 1 do on a RefCell?
- Reads the value
- Clones the cell
- Panics always
- Mutably borrows and increments it
Answer: Mutably borrows and increments it. borrow_mut takes a mutable runtime borrow so you can change the value.
What is the classic combo for shared, mutable, single-threaded state?
- Box<Rc<T>>
- Rc<RefCell<T>>
- Arc<Mutex<T>>
- Vec<Box<T>>
Answer: Rc<RefCell<T>>. Rc<RefCell<T>> gives shared ownership plus interior mutability.
What runtime error happens with two overlapping mutable RefCell borrows?
- A compile error
- Silent corruption
- BorrowMutError panic
- Nothing
Answer: BorrowMutError panic. RefCell enforces borrow rules at runtime, panicking with BorrowMutError.
Continue this course
- Previous: Iterators
- Next: Concurrency & Threads — Spawn threads and pass messages safely with Rust's fearless concurrency
- Quick reference: Rust cheat sheet
Frequently asked questions
What is a smart pointer?
A smart pointer is a data structure that acts like a pointer but also has extra capabilities and metadata. In Rust the most common are Box, Rc, and RefCell. Box<T> owns a value on the heap; Rc<T> tracks how many owners share a value; RefCell<T> enforces borrowing rules at runtime to allow interior mutability. They implement traits like Deref (so you can use them like references) and Drop (so cleanup happens automatically), which is why they feel like ordinary references while doing more.
When should I use Box<T>?
Use Box<T> when you want to store a value on the heap behind a single owner. Two situations make it necessary: when you have a recursive type, such as a linked list or a tree, whose size cannot be known at compile time without indirection; and when you have a large value you want to allocate on the heap rather than the stack. Box is also used to create trait objects, as in Box<dyn Trait>, to store values of different concrete types behind one type.
What does Rc do, and is it thread-safe?
Rc stands for Reference Counted. It lets multiple parts of your program share ownership of the same heap data: each Rc::clone increases a counter, and the data is freed only when the last owner is dropped. This is useful for structures like graphs where a node may have several owners. However, Rc is NOT thread-safe — it is meant for single-threaded use. For sharing across threads you use its atomic counterpart, Arc, which you will meet in the concurrency lesson.
Why would I use RefCell instead of a normal mutable reference?
RefCell provides interior mutability: it lets you mutate data even when you only hold a shared reference, by moving the borrow checking from compile time to runtime. This is essential in situations where the compiler cannot prove your borrows are safe but you know they are — most famously when combined with Rc as Rc<RefCell<T>> to get shared, mutable data. The trade-off is that if you violate the borrow rules at runtime (for example, two simultaneous mutable borrows), the program panics instead of failing to compile.