Can you write this from memory?
Create a Box containing the value 5 and store it in a variable `b`.
Rust's smart pointers are data structures that act like pointers but have additional metadata and capabilities. The ownership system handles most cases, but sometimes you need more flexibility.
Box<T> puts data on the heap. Rc<T> enables shared ownership. RefCell<T> provides interior mutability. Arc<T> is Rc for threads. Weak<T> breaks reference cycles.
This page focuses on when and how to use each smart pointer, and the errors you hit when you pick the wrong one.
Before reaching for smart pointers, exhaust simpler options:
T,&T,&mut T— regular ownership and borrowing covers most casesVec<T>,String— owned heap buffers with simple ownership- Smart pointers — only when the ownership graph demands it
If you're wrapping everything in Box or Rc, step back and ask whether borrows or restructuring would work. The ownership and borrowing guide covers the borrow-first approach that eliminates most smart pointer needs.
A type that directly contains itself has no finite size. Box adds indirection with a known pointer size:
// WRONG: infinite size
// enum List {
// Cons(i32, List), // error[E0072]: recursive type has infinite size
// Nil,
// }
// RIGHT: Box adds indirection
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Rc uses non-atomic reference counting and does not implement Send:
// WRONG: Rc is not thread-safe
// use std::rc::Rc;
// let data = Rc::new(vec![1, 2, 3]);
// thread::spawn(move || {
// println!("{data:?}"); // error[E0277]: Rc cannot be sent between threads
// });
// RIGHT: Arc uses atomic reference counting
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("{data_clone:?}");
}).join().unwrap();
Note: Arc<T> gives you shared ownership, not shared mutation. T must still be Send + Sync to be safely shared. For mutation, pair with Mutex or RwLock. The concurrency guide covers the Arc<Mutex<T>> and channel patterns in depth.
RefCell checks borrow rules at runtime. Calling .borrow_mut() while a .borrow() is active panics:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
// WRONG: immutable borrow still active when we try to mutate
// let borrowed = data.borrow();
// data.borrow_mut().push(4); // panic: already borrowed: BorrowMutError
// RIGHT: drop the immutable borrow first
{
let borrowed = data.borrow();
println!("{:?}", *borrowed);
} // borrowed drops here
data.borrow_mut().push(4); // OK
// Or use try_borrow_mut for fallible borrowing (no panic)
match data.try_borrow_mut() {
Ok(mut guard) => guard.push(5),
Err(_) => eprintln!("already borrowed"),
}
| Situation | Use |
|---|---|
| Single owner, heap allocation needed | Box<T> |
| Recursive type (tree, linked list) | Box<T> |
| Multiple owners, single-threaded | Rc<T> |
| Multiple owners, multi-threaded | Arc<T> |
| Mutation through shared/immutable handle (single-threaded) | RefCell<T> |
| Shared + mutable, single-threaded | Rc<RefCell<T>> |
| Shared + mutable, multi-threaded | Arc<Mutex<T>> or Arc<RwLock<T>> |
| Back-pointer / parent link (break cycles) | Weak<T> |
RefCell<T> is for interior mutability — mutating through an immutable reference, with runtime borrow checks. If you have single ownership and can use &mut T, you don't need RefCell.
// Put data on the heap
let b = Box::new(5);
println!("{}", *b); // dereference with *
// Box for trait objects
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
Box has zero runtime cost beyond heap allocation — no reference counting, no runtime checks.
Rc cycles prevent deallocation — neither value's count reaches zero. Use Weak for back-pointers:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // Weak: doesn't keep parent alive
children: RefCell<Vec<Rc<Node>>>, // Rc: keeps children alive
}
let parent = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Rc::downgrade(&parent)),
children: RefCell::new(vec![]),
});
parent.children.borrow_mut().push(Rc::clone(&child));
// Upgrade Weak to access the parent (returns Option<Rc<Node>>)
if let Some(p) = child.parent.borrow().upgrade() {
println!("parent value: {}", p.value);
}
println!("strong: {}, weak: {}",
Rc::strong_count(&parent), // 1
Rc::weak_count(&parent), // 1
);
Pattern: children hold Rc (keep child alive), parents hold Weak (don't prevent deallocation).
Arc provides shared ownership, not shared mutation. For read-only sharing, no Mutex needed:
use std::sync::Arc;
use std::thread;
let config = Arc::new(String::from("production"));
let mut handles = vec![];
for _ in 0..3 {
let config = Arc::clone(&config);
handles.push(thread::spawn(move || {
println!("mode: {config}"); // read-only — no Mutex needed
}));
}
for h in handles { h.join().unwrap(); }
For read-heavy patterns with occasional writes, use Arc<RwLock<T>> instead of Arc<Mutex<T>> — it allows concurrent readers.
When you have a unique Arc (no other clones), Arc::get_mut() gives &mut T without a lock. Arc::make_mut() provides clone-on-write semantics. Understanding references and lifetimes helps explain why Rc and Arc exist -- they solve cases where the borrow checker cannot statically prove a single owner.
- The Rust Book: Smart Pointers — Box, Rc, RefCell, reference cycles
- The Rust Book: Reference Cycles — Weak, tree patterns
- std::rc::Rc — strong_count, weak_count, Weak API
- std::sync::Arc — get_mut, make_mut, thread-safety requirements
When to Use Rust Smart Pointers: Box, Rc, RefCell & Arc
- Use `Box<T>` for recursive types and trait objects.
- Use `Rc<T>` when multiple parts of code need to read the same data (single-threaded).
- Use `Arc<T>` for shared ownership across threads.
- Use `RefCell<T>` for interior mutability (mutation through an immutable handle).
- Use `Rc<RefCell<T>>` for shared mutable data in single-threaded code.
- Use `Arc<Mutex<T>>` or `Arc<RwLock<T>>` for shared mutable data across threads.
Check Your Understanding: Rust Smart Pointers: Box, Rc, RefCell & Arc
When would you use Rc<RefCell<T>> versus Arc<Mutex<T>>?
Rc<RefCell<T>> is for single-threaded shared mutable state—cheaper but not thread-safe. Arc<Mutex<T>> is for multi-threaded shared mutable state—thread-safe but with locking overhead.
What You'll Practice: Rust Smart Pointers: Box, Rc, RefCell & Arc
Common Rust Smart Pointers: Box, Rc, RefCell & Arc Pitfalls
- Symptom: `error[E0277]: Rc<T> cannot be sent between threads safely`. Why: `Rc` uses non-atomic reference counting and does not implement `Send`. Fix: Replace `Rc` with `Arc` for any data shared across threads.
- Symptom: Thread panics with `already borrowed: BorrowMutError`. Why: `RefCell` enforces borrow rules at runtime, and you called `.borrow_mut()` while a `.borrow()` was still active. Fix: Restructure so the immutable borrow is dropped before you mutably borrow, or use scoped blocks to limit borrow lifetimes.
- Symptom: Memory keeps growing, `Rc::strong_count()` never reaches zero. Why: Two `Rc` values point to each other, creating a reference cycle that can never be freed. Fix: Use `Rc::downgrade()` to create `Weak<T>` references for back-pointers or parent links.
- Symptom: `error[E0072]: recursive type has infinite size`. Why: The type directly contains itself, so the compiler cannot calculate a finite size. Fix: Wrap the recursive field in `Box<T>` to add a level of indirection with a known pointer size.
- Symptom: Littering `Box::new()`, `Rc::new()`, and `.clone()` everywhere. Why: Smart pointers add heap allocation and indirection overhead. Fix: Only reach for smart pointers when regular ownership or borrowing genuinely cannot express what you need — most code works fine with `&T` and `&mut T`.
Rust Smart Pointers: Box, Rc, RefCell & Arc FAQ
Why do recursive types need Box?
Without indirection, the compiler cannot determine the size of a recursive type. Box provides a fixed-size pointer to heap data, making the size known at compile time.
What happens if I violate RefCell borrow rules?
RefCell checks borrows at runtime. If you try to borrow mutably while already borrowed (or vice versa), it panics. Use `try_borrow()` and `try_borrow_mut()` for fallible borrowing.
How do I avoid reference cycles with Rc?
Use `Weak<T>` for references that should not keep data alive. Create weak references with `Rc::downgrade()` and upgrade to `Rc` with `.upgrade()` when needed.
What is the performance cost of Rc vs Box?
Box has zero runtime cost for ownership. Rc has reference counting overhead on clone and drop. Arc is more expensive than Rc due to atomic operations.
Can I use Arc without Mutex?
Yes, if you only need shared immutable access. Arc<T> allows multiple threads to read T. For mutation, pair with Mutex or RwLock. For read-heavy workloads, RwLock allows concurrent readers.
Does Arc make T thread-safe?
Arc provides shared ownership with atomic reference counting, but T must still be Send + Sync to be shared safely. Arc<T> is Send + Sync only when T is Send + Sync.
What is the Sync analog of RefCell?
RwLock<T> provides the same read/write separation as RefCell, but with thread-safe locking instead of runtime borrow checks. For read-heavy patterns across threads, use Arc<RwLock<T>>.
Rust Smart Pointers: Box, Rc, RefCell & Arc Syntax Quick Reference
let b = Box::new(5);let x = *b;let a = Rc::new(5);let b = Rc::clone(&a);Rc::strong_count(&a)let cell = RefCell::new(5);let val = cell.borrow();*cell.borrow_mut() += 1;let a = Arc::new(5);if let Ok(mut g) = cell.try_borrow_mut() {
*g += 1;
}let weak = Rc::downgrade(&rc);if let Some(rc) = weak.upgrade() {
println!("{}", rc);
}Rust Smart Pointers: Box, Rc, RefCell & Arc Sample Exercises
Fill in the operator to dereference the Box and get the inner value.
*Fill in the smart pointer needed for recursive data structures.
BoxFill in the smart pointer for dynamic dispatch.
Box+ 36 more exercises