When one owner isn't enough
You're building a drawing app. You draw a circle. That circle lives in memory. Now you want to move it to a "selected items" list. Then you realize the user wants to copy that circle to the clipboard, but the original circle must stay on the canvas. Finally, you want to save the canvas to disk in the background without freezing the UI. Suddenly, that simple circle needs three different ways to exist in memory. Rust gives you three tools for this exact pressure: Box, Rc, and Arc. They all put data on the heap, but they solve different ownership problems. Pick the wrong one and the compiler screams. Pick the right one and your memory management becomes invisible.
The three shapes of heap data
Rust's default is stack allocation with single ownership. When a variable goes out of scope, the data vanishes. This is fast and safe. Sometimes the data is too big for the stack, or multiple parts of your code need to point to the same data. That's where heap allocation comes in. The heap is a large pool of memory managed by the allocator. You can allocate as much as the system allows. Pointers to the heap are small and fit on the stack. Box, Rc, and Arc are all stack-sized wrappers that point to data on the heap. The difference is who gets to keep the data alive.
Box<T> is single ownership. One owner. When the owner drops, the data dies. Box is essential when you need to transfer ownership of large data, or when you need a trait object like Box<dyn Trait>. The compiler requires variables to have a known size. A trait object has an unknown size because it could be any type. Box has a known size (the pointer), so Box<dyn Trait> works. The data behind the box can be any size.
Rc<T> stands for Reference Counted. Multiple owners. A counter tracks how many owners exist. When the counter hits zero, the data dies. Rc is for single-threaded code. It uses a regular integer for the counter, which is fast. Rc shines in graph structures, UI component trees, and caches where ownership is distributed. Rc also supports Weak references to break cycles. A Weak reference doesn't increment the counter. If you have a parent-child relationship where both point to each other, Rc alone leaks. Use Rc for the parent-to-child link and Weak for the child-to-parent link. When the parent drops, the child drops, and the Weak becomes invalid.
Arc<T> is Atomic Reference Counted. Same as Rc, but the counter updates are thread-safe. You can share Arc across threads. You cannot share Rc across threads. Arc uses atomic operations for the counter. Atomics are CPU instructions that prevent race conditions when multiple threads update memory at once. They are slightly slower than regular integers. That cost is why Rc exists. If you don't have threads, don't pay the atomic tax.
Choose the wrapper that matches your ownership reality. The compiler enforces the contract.
Minimal examples and convention
use std::rc::Rc;
use std::sync::Arc;
fn main() {
// Box: single owner, heap allocated.
// The Box itself is small and lives on the stack.
// The data (42) lives on the heap.
let single_owner = Box::new(42);
// Rc: shared ownership, single thread.
// Cloning an Rc doesn't copy the data.
// It just increments a counter.
let shared = Rc::new(42);
let shared_clone = Rc::clone(&shared);
// Arc: shared ownership, multi-thread safe.
// Same as Rc, but uses atomic operations for the counter.
// Safe to send to another thread.
let atomic_shared = Arc::new(42);
let atomic_clone = Arc::clone(&atomic_shared);
}
You'll see Rc::clone(&shared) in the code above. You could write shared.clone(), and it compiles. The community prefers the explicit Rc::clone form. Why? Because clone() usually implies a deep copy of data. With Rc, cloning is cheap; it just bumps a counter. Writing Rc::clone signals to the reader: "This is a pointer copy, not a data copy." It prevents the mental model mismatch where a reader expects expensive work and finds cheap work instead. The same convention applies to Arc::clone. Use the explicit form to document intent.
What happens under the hood
When you create a Box, Rust allocates memory on the heap and returns a pointer. The Box struct on your stack holds that pointer. When the Box variable leaves scope, Rust calls drop on the pointer, freeing the heap memory. Simple. Box has zero overhead compared to a raw pointer, aside from the allocation itself.
Rc adds a counter. Rc::new allocates the data and a counter set to 1. Rc::clone increments the counter. Dropping an Rc decrements the counter. If the counter is not zero, the data stays. If it hits zero, the data is freed. The counter lives on the heap alongside the data. This means every Rc clone points to the same heap allocation. The cost is the counter update and the heap allocation. Rc is faster than Arc because the counter is a plain integer. No atomic instructions are needed.
Arc is identical to Rc except the counter uses atomic operations. Atomics ensure that if two threads increment the counter at the same time, the result is correct. Without atomics, two threads could read the same count, increment it, and write it back, losing one increment. That leads to use-after-free or memory leaks. Arc prevents this. The cost is the atomic instruction, which is more expensive than a regular integer add. On some architectures, atomics also introduce memory barriers that slow down surrounding code. Use Arc only when threads are involved.
The mutation trap
Rc and Arc provide shared ownership, but they provide immutable access by default. You can read the data, but you cannot change it. This aligns with Rust's rule: you can have many immutable references or one mutable reference, but not both. If you have multiple Rc clones, you have multiple references. Rust forbids mutable access. To mutate shared data, you need interior mutability.
For Rc, use Rc<RefCell<T>>. For Arc, use Arc<Mutex<T>>. RefCell checks borrowing rules at runtime. Mutex checks at runtime and is thread-safe. This is a common stumbling block. Beginners create an Rc and try to modify a field, getting a "cannot assign" error. The solution is wrapping the inner type in RefCell or Mutex.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Rc alone is immutable.
// Rc<RefCell<T>> allows mutation with runtime checks.
let shared = Rc::new(RefCell::new(5));
// Borrow mutably to change the value.
// borrow_mut() returns a RefMut guard.
// The guard enforces that no other borrows exist.
*shared.borrow_mut() += 10;
// Read the value.
// borrow() returns a Ref guard.
println!("Value: {}", *shared.borrow());
}
RefCell::borrow_mut panics if the data is already borrowed. This is a runtime check. Rust moves the borrow checker from compile time to runtime for RefCell. This is safe, but it costs a check at runtime. Use RefCell when you need shared mutation in a single thread and you can't satisfy the compile-time borrow checker. Mutex is the thread-safe equivalent. It blocks other threads while you hold the lock. Shared ownership does not imply shared mutation. Wrap the inner type in RefCell or Mutex before you try to change the data.
Box versus references
Sometimes you don't need Box. If you just need to point to data that lives elsewhere, a reference &T is better. References have zero overhead and enforce lifetimes. Box allocates memory. Allocation is slower than a pointer copy. Use Box when you need ownership. Use references when you need to borrow. If you find yourself using Box just to pass data around without sharing ownership, you might be over-allocating. Check if a reference suffices.
However, Box is essential for trait objects. You cannot have a reference to a trait object without a lifetime that binds it to the concrete type in many cases, and you cannot store &dyn Trait in a struct easily without complex lifetime annotations. Box<dyn Trait> is the standard way to store polymorphic data. The Box owns the data, so the lifetime is managed by the ownership rules. No complex annotations needed. References borrow. Boxes own. Allocate only when you must transfer ownership or break a lifetime cycle.
Compiler errors and pitfalls
Trying to share Rc across threads triggers a trait bound error. The compiler complains that Rc does not implement Send. This is a hard error. The compiler protects you from data races. Rc uses non-atomic counters. Two threads incrementing the counter at the same time can corrupt the count. Arc fixes this with atomics. If you see E0277 (trait bound not satisfied) mentioning Send and Rc, switch to Arc.
If you try to mutate a value inside an Arc, you hit a wall. Arc<T> gives you shared references, not mutable ones. You cannot change the data through an Arc. You need Arc<Mutex<T>> for shared mutation. If you try to assign to a field behind an Arc, the compiler rejects it with a "cannot assign" error. Wrap the inner type in Mutex.
Box behaves like a normal value for ownership. Moving a Box transfers ownership. Using the old variable name after the move triggers E0382 (use of moved value). The data is gone from that variable. This is the same error you get with any owned type. Box doesn't change the ownership rules; it just moves the data to the heap. If you need to use the data after moving it, clone it first, or pass a reference.
The compiler blocks Rc in threads to save you from silent memory corruption. Trust the error. Switch to Arc.
Decision matrix
Use Box<T> when you have a single owner and the data is large or has a dynamic size. Use Box<T> when you need to store a trait object like Box<dyn Trait> because the compiler requires a known size for the pointer. Use Rc<T> when multiple parts of your single-threaded code need to share ownership of the same data. Use Rc<T> for graph structures, UI trees, or caches where ownership is distributed. Use Arc<T> when you need to share data across multiple threads. Use Arc<T> when the data is read-only and shared, or when combined with Mutex for shared mutation. Use Arc<T> only when threading requires it; Arc carries an atomic performance cost that Rc avoids.
Start with Box. Reach for Rc when ownership splits. Reach for Arc when threads join the party. Never pay for atomics you don't need.