Rust Smart Pointers Explained: Box, Rc, Arc and RefCell in 2026
Rust smart pointers Box, Rc, Arc and RefCell explained with compilable 2026 examples, a decision table and common interview questions.

Rust smart pointers are the tools that unlock ownership patterns the borrow checker alone cannot express: heap allocation, shared ownership, and mutation behind a shared reference. Where a plain reference (&T) only borrows a value, a smart pointer owns its data and layers extra behavior on top of it. This guide breaks down the four types every Rust developer meets in production and in interviews: Box, Rc, Arc and RefCell, with compilable examples targeting the Rust 2024 edition.
Use Box<T> for single-owner heap allocation, Rc<T> for shared ownership on one thread, Arc<T> for shared ownership across threads, and RefCell<T> to mutate a value through a shared reference. The pairs Rc<RefCell<T>> and Arc<Mutex<T>> cover shared mutable state.
What smart pointers are in Rust
A smart pointer is a struct that behaves like a pointer but carries extra metadata or capabilities. Most implement the Deref trait, so *pointer and method calls work as if the pointer were a plain reference, and the Drop trait, so cleanup runs automatically when the value leaves scope. The standard library ships the four covered here, and understanding them depends on a solid grasp of ownership and borrowing, which decides who frees each allocation and when.
The key distinction from a reference is ownership. &T never owns the data it points at, so it cannot outlive the value. A smart pointer owns the data, controls its lifetime, and frees it deterministically. The Rust Book chapter on smart pointers treats them as a category precisely because they share this ownership-plus-behavior shape.
Box<T>: heap allocation for recursive and sized types
Box<T> is the simplest smart pointer. It stores a value on the heap and holds a pointer to it on the stack, with a single owner and no runtime overhead beyond the allocation itself. Its most common job is giving recursive types a known size: a type that contains itself directly would be infinitely large, but a Box is just a pointer, so its size is fixed regardless of what it points to.
The example below defines a binary tree. Each Node holds two children, and without indirection the compiler cannot compute the size of Tree. Wrapping each child in a Box breaks the recursion at the type level.
#[derive(Debug)]
enum Tree {
Leaf(i32),
// Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
Node(Box<Tree>, Box<Tree>),
}
fn sum(tree: &Tree) -> i32 {
match tree {
Tree::Leaf(value) => *value, // base case: return the leaf
Tree::Node(left, right) => sum(left) + sum(right), // recurse into both children
}
}
fn main() {
// Build (1) + ((2) + (3)) = 6
let tree = Tree::Node(
Box::new(Tree::Leaf(1)),
Box::new(Tree::Node(
Box::new(Tree::Leaf(2)),
Box::new(Tree::Leaf(3)),
)),
);
println!("sum = {}", sum(&tree)); // sum = 6
}Box also matters when moving a large value would be expensive, or when returning a trait object such as Box<dyn Error>. In every case the rule is the same: one owner, freed automatically when the Box drops.
Rc<T>: shared ownership in single-threaded code
Sometimes a value needs several owners, none of which is obviously the last to use it. Rc<T> (reference counted) solves this by keeping a strong count of how many owners exist. Rc::clone increments that count without copying the underlying data, and each drop decrements it. When the count hits zero, the value is freed.
Consider a configuration object that several workers read. Each worker should hold the config for as long as it needs it, and the allocation should disappear only when the last worker is gone.
use std::rc::Rc;
#[derive(Debug)]
struct Config {
endpoint: String,
timeout_ms: u32,
}
fn main() {
// Rc::new moves Config onto the heap with a strong count of 1.
let config = Rc::new(Config {
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
});
// Rc::clone only bumps the reference count; it does not deep-copy Config.
let worker_a = Rc::clone(&config);
let worker_b = Rc::clone(&config);
// All three handles point to the same allocation.
println!("endpoint: {}", worker_a.endpoint);
println!("timeout: {}", worker_b.timeout_ms);
// strong_count reports how many owners currently hold the value.
println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}Calling Rc::clone explicitly (rather than config.clone()) is idiomatic: it signals to readers that the operation is cheap and only touches a counter. The catch is that Rc<T> hands out shared, immutable references only. It cannot mutate the value it shares, and it is not safe to send across threads. The Rc documentation spells out both constraints.
RefCell<T> and interior mutability in Rust
Rust normally enforces that a value is either shared through many immutable references or mutated through exactly one mutable reference, and it checks this at compile time. RefCell<T> provides interior mutability: it lets code mutate a value through a shared reference by moving that same check to runtime. borrow() returns a shared read guard; borrow_mut() returns an exclusive write guard. The rules are identical, but violations panic instead of failing to compile.
Combined with Rc, this produces the canonical single-threaded shared-mutable pattern, Rc<RefCell<T>>: several owners that can all update the same value.
use std::rc::Rc;
use std::cell::RefCell;
// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;
fn increment(counter: &SharedCounter) {
// borrow_mut() hands out an exclusive reference, checked at runtime.
*counter.borrow_mut() += 1;
}
fn main() {
let counter: SharedCounter = Rc::new(RefCell::new(0));
let handle_a = Rc::clone(&counter);
let handle_b = Rc::clone(&counter);
increment(&handle_a);
increment(&handle_b);
increment(&counter);
// borrow() gives a shared read guard; the value is now 3.
println!("count = {}", counter.borrow()); // count = 3
}Calling borrow_mut() while another borrow() or borrow_mut() guard is still alive panics with already borrowed: BorrowMutError. The safety guarantees hold, but a logic bug that the compiler would have caught becomes a crash at runtime. Keep guards short-lived and avoid holding a borrow across a function call that might re-enter the same RefCell.
Interior mutability is also where reference cycles hide. Two Rc<RefCell<T>> values that point at each other keep each other's strong count above zero forever, so neither is ever freed. The fix is Weak<T>, a non-owning handle that does not affect the strong count; the classic walkthrough lives in Learn Rust With Entirely Too Many Linked Lists.
Arc<T>: thread-safe reference counting for concurrency
Arc<T> (atomically reference counted) is the multi-threaded sibling of Rc<T>. The public API is nearly identical, but the counter uses atomic operations, so cloning and dropping an Arc from several threads at once stays correct. That atomicity is why Arc is chosen only when sharing crosses a thread boundary: the atomic increment is measurably slower than the plain one Rc uses.
Because Arc<T> still hands out immutable references, mutation across threads needs a synchronization primitive. Mutex<T> is the usual partner, giving the Arc<Mutex<T>> pattern that mirrors Rc<RefCell<T>> for concurrent code. The example spawns four threads that each increment a shared total a thousand times.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc is safe to share across threads; Mutex serializes access to the inner value.
let total = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for _ in 0..4 {
// Each thread gets its own Arc handle (an atomic count bump).
let total = Arc::clone(&total);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() blocks until the mutex is free, then returns a guard.
let mut value = total.lock().unwrap();
*value += 1;
}
}));
}
for handle in handles {
handle.join().unwrap(); // wait for every thread before reading
}
println!("total = {}", *total.lock().unwrap()); // total = 4000
}The compiler enforces the boundary for you: Rc<T> is not Send, so trying to move one into thread::spawn fails to compile, pushing you toward Arc. Reaching for Arc inside a single thread just pays for atomics you never use. The Arc documentation covers the memory-ordering guarantees in detail, and Arc<Mutex<T>> underpins much of the shared state in async Rust with Tokio.
Ready to ace your Rust interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Box vs Rc vs Arc vs RefCell: when to use each
The four types compose along two axes: how many owners a value has, and whether it can be mutated through a shared handle. The table summarizes the trade-offs.
| Type | Ownership | Mutation via shared handle | Thread-safe | Overhead |
|------|-----------|----------------------------|-------------|----------|
| Box<T> | Single | No | Yes, if T: Send | Heap allocation only |
| Rc<T> | Shared | No | No | Non-atomic count |
| Arc<T> | Shared | No | Yes | Atomic count |
| RefCell<T> | Single | Yes (runtime-checked) | No | Runtime borrow flag |
The practical decision tree is short. Need one owner on the heap: Box. Need many owners on one thread: Rc. Need many owners across threads: Arc. Need to mutate a shared value: wrap the inner type in RefCell (single-thread) or Mutex (multi-thread). The Effective Rust guidance on reference and pointer types reaches the same layering. Start with the least powerful option and add capability only when the compiler forces it.
Rust smart pointers interview questions
Smart pointers are a reliable interview topic because they test whether a candidate understands ownership rather than just syntax. The questions below appear often, and more are collected in the Rust smart pointers interview module.
What is the difference between Rc and Arc? Both provide shared ownership through reference counting. Rc uses a non-atomic counter and is confined to a single thread; Arc uses atomic operations and can be shared across threads at a small runtime cost. The compiler enforces the split: Rc is neither Send nor Sync, so it cannot cross a thread boundary.
Why combine Rc with RefCell? Rc grants shared ownership but only immutable access. RefCell adds interior mutability, letting owners mutate the value through a shared reference with runtime borrow checks. Together, Rc<RefCell<T>> is the idiomatic single-threaded shared-mutable building block.
How can reference counting leak memory in Rust? Two Rc (or Arc) values that reference each other form a cycle whose strong counts never reach zero, so the allocation is never freed. Breaking the cycle with Weak<T>, which holds a non-owning reference, restores correct cleanup.
When is Box preferable to Rc? Whenever a single owner suffices. Box has no reference-counting overhead, so it is the default for heap allocation, recursive types, and trait objects. Reach for Rc only when genuine shared ownership is required.
Conclusion
Rust smart pointers turn ownership from a constraint into a set of composable building blocks. The choice among them follows directly from the shape of the problem:
- Reach for
Box<T>when a value needs a single heap owner, a recursive type needs a fixed size, or a function returns a trait object. - Use
Rc<T>for multiple owners on one thread, and switch toArc<T>the moment ownership crosses a thread boundary. - Add
RefCell<T>for interior mutability in single-threaded code, andMutex<T>for the concurrent equivalent, keeping borrow and lock guards short-lived. - Combine them deliberately:
Rc<RefCell<T>>for shared mutable state on one thread,Arc<Mutex<T>>across threads. - Watch for reference cycles between counted pointers and break them with
Weak<T>to avoid leaks. - Default to the least powerful type and let the compiler tell you when more capability is required.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

Rust Traits and Generics in 2026: Trait Upcasting, AsyncFn and Advanced Patterns
Master Rust traits and generics with the latest 2024 Edition features: trait upcasting, AsyncFn closures, RPITIT, and advanced patterns tested in real interviews.

Rust Ownership and Borrowing: The Guide That Demystifies Everything
Master Rust ownership and borrowing with practical examples. Understand move semantics, references, lifetimes, and the borrow checker to write safe, efficient Rust code.

Rust Interview Questions: Complete Guide 2026
The 25 most common Rust interview questions. Ownership, borrowing, lifetimes, traits, async and concurrency with detailed answers and code examples.