Rust's Box, Rc, and Arc types all store a value on the heap, but they express different ownership relationships. Box gives the value one owner. Rc and Arc let several owners share a value by keeping a reference count. Choosing between them is primarily about ownership, not merely where the value is stored.
Use Box for one owner and heap allocation
Box<T> owns a T and stores it on the heap. When the box goes out of scope, both the value and its heap allocation are dropped. Moving a box transfers its one owner; it does not copy the value.
let message = Box::new(String::from("hello"));
let moved_message = message;
assert_eq!(moved_message.as_str(), "hello");
// `message` can no longer be used because ownership moved.
A box is useful when a value must have a known size while the program compiles, even though its contents do not. Recursive types are a common example:
enum List {
Item(i32, Box<List>),
End,
}
let list = List::Item(1, Box::new(List::Item(2, Box::new(List::End))));
Without Box, each List::Item would contain another complete List, so the type would have no finite size. The box has a known size and points to the next node on the heap. Boxes are also useful for trait objects such as Box<dyn std::error::Error>, where the concrete value's type or size can vary.
Do not use Box when ordinary ownership is enough
Heap allocation has a cost. A local String, Vec<T>, or struct is already an owned value and does not need to be wrapped in Box just to transfer ownership. Use Box when you specifically need indirection, a recursive type, a trait object, or a large value kept off the stack.
Box does not provide shared ownership. Borrow it with &T for temporary access, or choose Rc or Arc when several parts of a program must independently keep the value alive.
Use Rc for shared ownership in one thread
Rc<T> means “reference-counted T”. It is useful when several parts of a single-threaded program need to own the same value, and the value should be dropped only after the last owner goes away.
use std::rc::Rc;
let name = Rc::new(String::from("Ada"));
let first_owner = Rc::clone(&name);
let second_owner = Rc::clone(&name);
assert_eq!(first_owner.as_str(), "Ada");
assert_eq!(Rc::strong_count(&name), 3);
This is common in trees, graphs, and other structures where a value has multiple owners. The shared value is read-only through Rc alone. If it must be mutated, Rc<RefCell<T>> provides checked-at-runtime interior mutability:
use std::{cell::RefCell, rc::Rc};
let count = Rc::new(RefCell::new(0));
*count.borrow_mut() += 1;
Do not use Rc when borrowing or moving is enough
Rc adds reference-counting overhead beyond the heap allocation. Prefer an ordinary reference (&T) when a function only needs temporary access, and prefer ordinary ownership or Box<T> when there is one clear owner.
Rc is also not thread-safe: it cannot be sent between threads or used where Rust requires Send or Sync. Do not use it for data shared by worker threads, and be careful with Rc<RefCell<T>>: it can create reference cycles, which keep values alive forever. Use Weak links for back-references when cycles are possible.
Use Arc for shared ownership across threads
Arc<T> is an atomically reference-counted pointer. It has the same basic ownership model as Rc, but its counter is safe to update from multiple threads.
use std::sync::Arc;
use std::thread;
let message = Arc::new(String::from("hello"));
let worker_message = Arc::clone(&message);
let handle = thread::spawn(move || {
println!("{worker_message}");
});
handle.join().unwrap();
Use Arc when multiple threads need to own the same read-only data, such as configuration, a lookup table, or application state. For shared mutable data, combine it with a synchronization primitive such as Mutex or RwLock, for example Arc<Mutex<T>>.
Do not use Arc when threads are not involved
Arc has atomic operations and is generally more expensive than Rc. If the value never leaves one thread, Rc is the simpler choice. If there is only one owner, use ordinary ownership or Box instead of either reference-counted pointer.
Also, Arc does not make the contained value magically thread-safe. Arc<RefCell<T>> is still not suitable for sharing between threads, and Arc<T> only works across threads when T satisfies the required Send and Sync bounds. Use a Mutex, RwLock, atomics, or a message-passing channel when concurrent mutation is required.
Practical rule
- One owner without indirection: ordinary ownership.
- One owner with heap allocation or indirection:
Box<T>.
- Borrowed access:
&T or &mut T.
- Multiple owners in one thread:
Rc<T>.
- Multiple owners across threads:
Arc<T>.
- Shared mutation: add the appropriate interior-mutability or synchronization type.
Start with ordinary ownership and borrowing. Choose Box, Rc, or Arc only when the value's required ownership relationship calls for it.