Rust Smart Pointers Box Rc and RefCell
In Rust, every value has a single owner. That rule keeps memory safe. But sometimes you need more flexibility — you want to store data on the heap, share ownership between multiple parts of your code, or mutate data even when the compiler thinks it should stay read-only. Smart pointers solve all three of these problems.
A smart pointer looks like a regular pointer, but it carries extra logic. It manages memory, tracks how many owners a value has, and enforces Rust's rules — either at compile time or at runtime. Rust gives you three essential smart pointers: Box, Rc, and RefCell.
What Is a Smart Pointer?
A regular reference in Rust (&T) borrows a value temporarily. A smart pointer owns the value. It wraps the value inside a struct that implements two special traits: Deref (so it behaves like a reference) and Drop (so it automatically cleans up when it goes out of scope).
Think of a smart pointer like a parcel delivery system. A regular reference is like showing someone where a package is kept. A smart pointer is like handing them the package in a tracked box — the box knows who owns it, logs when it gets passed around, and disposes of itself properly when no one needs it anymore.
Box<T> — Storing Data on the Heap
Box<T> puts a value on the heap and gives you a pointer to it. The value itself lives in heap memory, while the pointer lives on the stack. When the Box goes out of scope, Rust automatically drops both.
Why Use Box?
The stack has a fixed size. You use Box when:
- You have a large value and don't want to copy it around
- You need a type whose size is unknown at compile time
- You want to own a value through a trait object
Box in Action — A Filing Cabinet Diagram
Picture a filing cabinet with two sections: Stack Drawer and Heap Cabinet.
┌──────────────────────────────┐
│ STACK DRAWER │
│ │
│ b → [pointer: 0xA100] │
│ │
└──────────────┬───────────────┘
│ points to
┌──────────────▼───────────────┐
│ HEAP CABINET │
│ │
│ Address 0xA100: value = 5 │
│ │
└──────────────────────────────┘
When you write let b = Box::new(5);, Rust stores 5 in the heap and keeps only a small pointer in the stack drawer labeled b. The pointer takes up just a few bytes. The actual data lives elsewhere.
fn main() {
let b = Box::new(5);
println!("b = {}", b); // prints: b = 5
}
You use b like a regular integer. Rust automatically dereferences the Box for you. When b goes out of scope, both the pointer and the heap value get cleaned up.
Recursive Types with Box
Rust needs to know the size of every type at compile time. But a recursive type — one that contains itself — has no fixed size. Box solves this.
Imagine a train made of carriages. Each carriage links to the next one, and the last one has a stop sign. This is a linked list.
[Carriage: 1] → [Carriage: 2] → [Carriage: 3] → [Stop]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Without Box, Rust cannot calculate how much memory List needs because it would contain itself infinitely. The Box breaks the cycle — it has a known, fixed pointer size, so Rust stays happy.
Rc<T> — Shared Ownership with Reference Counting
Rc<T> stands for Reference Counted. It lets multiple parts of your program share ownership of the same value. Every time you clone an Rc, the internal counter goes up by one. Every time one of those clones goes out of scope, the counter goes down by one. When the counter reaches zero, Rust drops the value.
The Shared Projector Diagram
Three people in a meeting room all watch the same projector. No one owns the projector alone — they all share it. The projector stays on as long as at least one person is watching. When the last person leaves, the projector turns off.
┌──────────────────────────────┐
│ Rc<String> ("Meeting") │
│ reference count = 3 │
└────────────┬─────────────────┘
│
┌────────────┼────────────┐
│ │ │
Alice's Bob's Carol's
Rc clone Rc clone Rc clone
use std::rc::Rc;
fn main() {
let projector = Rc::new(String::from("Meeting"));
let alice = Rc::clone(&projector);
let bob = Rc::clone(&projector);
println!("Count: {}", Rc::strong_count(&projector)); // prints: Count: 3
println!("Alice sees: {}", alice);
println!("Bob sees: {}", bob);
}
Rc::clone does not copy the data. It only copies the pointer and increments the counter. When alice and bob go out of scope, the count drops back down. When projector also goes out of scope, the String gets dropped from memory.
Rc Is Single-Threaded Only
Rc<T> does not use any locking mechanism, so it only works in single-threaded code. For multi-threaded programs, Rust provides Arc<T> (Atomic Reference Counted), which uses atomic operations to safely track the count across threads.
Rc Cannot Mutate Its Contents
Rc<T> gives you shared read access. Multiple owners can read the same value, but none of them can change it. Rust's ownership rules prevent mutating something that has multiple owners. To get both sharing and mutation, you combine Rc with RefCell.
RefCell<T> — Interior Mutability at Runtime
Rust normally checks borrowing rules at compile time. RefCell<T> shifts those checks to runtime. This is called interior mutability — the cell looks immutable from the outside, but you can still mutate what's inside.
The Sealed Envelope Diagram
Imagine a sealed envelope at an office. Nobody can touch it without signing a logbook first. If someone is reading the envelope, others can queue to read too. But if someone wants to write in it, they must wait until everyone else has finished — and only one writer gets access at a time. The logbook tracks who currently holds the envelope.
┌──────────────────────────────────────────┐ │ RefCell<String> │ │ │ │ State: [ READING | WRITING | FREE ] │ │ │ │ .borrow() → shared read access │ │ .borrow_mut() → exclusive write access │ │ │ │ Violations panic at runtime, not │ │ compile time │ └──────────────────────────────────────────┘
use std::cell::RefCell;
fn main() {
let note = RefCell::new(String::from("Hello"));
{
let mut writer = note.borrow_mut();
writer.push_str(", world");
} // writer released here
let reader = note.borrow();
println!("{}", reader); // prints: Hello, world
}
If you try to borrow mutably while another borrow is still active, RefCell panics at runtime instead of refusing to compile. This gives you flexibility but shifts the responsibility of correctness to you.
When to Use RefCell
- You know at design time that your borrow rules are correct, but the compiler cannot verify them
- You write mock objects for testing that need to record calls while appearing immutable
- You need to mutate a value inside an otherwise immutable context
Combining Rc and RefCell — Shared Mutable Data
The most powerful pattern in single-threaded Rust puts Rc and RefCell together. You get multiple owners who can all mutate the same data.
The Shared Whiteboard Diagram
Three colleagues all share access to a whiteboard. Any one of them can pick up the marker and write on it, one at a time. Everyone sees the updated board immediately.
Alice Bob Carol
│ │ │
└────────────────┴────────────────┘
│
┌───────────▼─────────────┐
│ Rc<RefCell<Vec<i32>>> │
│ reference count = 3 │
│ current value: [1,2,3] │
└─────────────────────────┘
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let whiteboard = Rc::new(RefCell::new(vec![1, 2, 3]));
let alice = Rc::clone(&whiteboard);
let bob = Rc::clone(&whiteboard);
alice.borrow_mut().push(4);
bob.borrow_mut().push(5);
println!("{:?}", whiteboard.borrow()); // prints: [1, 2, 3, 4, 5]
}
Rc handles the sharing. RefCell handles the mutation. Together they give you a safe, single-threaded pattern for shared mutable state.
Memory Leaks and Reference Cycles
One danger of Rc<T> is a reference cycle. If two values hold Rc pointers to each other, their counts never reach zero. Neither gets dropped. This is a memory leak.
The Circular Chain Diagram
Node A Node B
┌──────────┐ ┌──────────┐
│ Rc to B ─┼─────────►│ Rc to A ─┼──────┐
└──────────┘ └──────────┘ │
▲ │
└─────────────────────────────────┘
Count A = 1 (held by B)
Count B = 1 (held by A)
Neither ever reaches 0 → memory leak
Rust provides Weak<T> to break these cycles. A Weak pointer does not increment the reference count. If the value gets dropped, the Weak pointer returns None when you try to use it.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
}
Use Rc for ownership relationships and Weak for non-owning back-references like parent pointers in a tree.
Choosing the Right Smart Pointer
Each smart pointer fits a specific situation. Use this decision guide:
| Situation | Smart Pointer |
|---|---|
| Single owner, data on the heap | Box<T> |
| Multiple owners, read-only, single thread | Rc<T> |
| Single owner, mutate behind shared reference | RefCell<T> |
| Multiple owners, shared mutation, single thread | Rc<RefCell<T>> |
| Multiple owners, any thread | Arc<T> |
| Back-reference without ownership | Weak<T> |
The Deref Trait — How Smart Pointers Behave Like References
Rust lets you use a Box<T> anywhere you would use a plain &T. This works because Box implements the Deref trait. The compiler automatically inserts dereference operations for you, a process called deref coercion.
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let boxed = Box::new(String::from("Rustacean"));
greet(&boxed); // Box → String → &str automatically
}
Rust applies Deref repeatedly until it reaches the target type. You never need to manually dereference through multiple layers.
The Drop Trait — Automatic Cleanup
Every smart pointer implements Drop. When the pointer goes out of scope, Rust calls drop automatically. You can implement Drop on your own types to run custom cleanup code — closing a file, releasing a lock, or freeing external memory.
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Releasing: {}", self.name);
}
}
fn main() {
let r = Resource { name: String::from("database connection") };
println!("Resource created");
} // prints: Releasing: database connection
Rust guarantees that drop runs exactly once, in reverse order of creation, when values go out of scope. You never need to call a destructor manually.
Key Takeaways
Box<T>allocates data on the heap with a single owner and known pointer sizeRc<T>shares ownership across multiple variables using a reference count, single-threaded onlyRefCell<T>moves borrow-checking to runtime and enables interior mutabilityRc<RefCell<T>>combines shared ownership with the ability to mutateWeak<T>prevents memory leaks caused by reference cycles- All smart pointers implement
DerefandDrop, making them transparent and self-cleaning
