Box<T> — Heap Allocation
Box<T> is Rust's simplest smart pointer. It allocates a value on the heap and stores
a pointer to it on the stack. When the Box goes out of scope, both the pointer and
the heap-allocated value are automatically dropped.
Most Rust values live on the stack by default. Box<T> is the standard way to
explicitly move a value to the heap when you need to.
Creating a Box
Wrap any value with Box::new() to heap-allocate it. The resulting Box<T> behaves
almost identically to the value itself — you can call methods on it directly and
dereference it with * to access the inner value.
fn main() {
// 5 is stored on the heap; b (the pointer) lives on the stack
let b = Box::new(5);
println!("b = {}", b); // auto-derefs to print 5
println!("*b = {}", *b); // explicit dereference also works
// Box<T> implements Deref, so method calls work transparently
let s = Box::new(String::from("hello"));
println!("length: {}", s.len());
} // b and s are dropped here; heap memory is freedb = 5 *b = 5 length: 5
b itself is a thin pointer on the stack — just the size of a usize. The value 5 lives on the heap. This is why Box<T> has a known, fixed size regardless of what T is.The Three Main Use Cases
Recursive types — when a type cannot have a known size at compile time because it refers to itself.
Large data transfers — when you want to move ownership of a large value without copying it on the stack.
Trait objects (
Box<dyn Trait>) — when you need to store values of different concrete types behind a uniform interface.
Use Case 1 — Recursive Types
Rust requires every type to have a known size at compile time. A recursive type that refers to itself directly breaks this rule — the compiler would need to account for infinite nesting.
The classic example is a cons list. This definition does not compile:
// This does NOT compile
enum List {
Cons(i32, List), // ERROR: recursive type has infinite size
Nil,
}The compiler reports: "recursive type List has infinite size".
The fix is to box the recursive reference. Because Box<T> is always one pointer
wide, the compiler can determine the exact size of List.
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
// Build: 1 -> 2 -> 3 -> Nil
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
// Walk the list
let mut current = &list;
loop {
match current {
Cons(val, next) => {
print!("{} -> ", val);
current = next;
}
Nil => {
println!("Nil");
break;
}
}
}
}1 -> 2 -> 3 -> Nil
Use Case 2 — Moving Large Data Without Copying
When you return or assign a large struct, Rust normally copies it on the stack. For very large types that can be expensive or even cause a stack overflow. Boxing the value means only the pointer moves — the data stays in place on the heap.
struct HeavyData {
buffer: [u8; 1_000_000], // 1 MB — unsafe to copy on the stack
}
fn create_data() -> Box<HeavyData> {
// Allocated directly on the heap; no large stack copy on return
Box::new(HeavyData {
buffer: [0u8; 1_000_000],
})
}
fn main() {
let data = create_data();
println!("first byte: {}", data.buffer[0]);
// 'data' is dropped here; 1 MB of heap memory is freed
}Box when the data is genuinely large or when you specifically need indirection.Use Case 3 — Trait Objects with Box<dyn Trait>
Trait objects let you store values of different concrete types behind the same pointer, enabling runtime polymorphism. Because different implementors of a trait can have different sizes, you need a level of indirection. Boxing each value gives a uniform, pointer-sized handle.
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
struct Cat;
struct Parrot { phrase: String }
impl Animal for Dog { fn speak(&self) -> &str { "Woof!" } }
impl Animal for Cat { fn speak(&self) -> &str { "Meow!" } }
impl Animal for Parrot { fn speak(&self) -> &str { &self.phrase } }
fn main() {
// Vec of trait objects — each element can be a different concrete type
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Parrot { phrase: String::from("Polly wants a cracker!") }),
];
for animal in &animals {
println!("{}", animal.speak());
}
}Woof! Meow! Polly wants a cracker!
dyn keyword signals dynamic dispatch — the correct method is looked up at runtime via a vtable rather than resolved at compile time. This small indirection cost enables powerful heterogeneous collections.Box<dyn Error> for Flexible Error Handling
A very common Rust pattern is returning Box<dyn Error> from main or any
fallible function where multiple different error types could be returned. The ?
operator automatically boxes any error that implements the Error trait.
use std::error::Error;
use std::fs;
fn parse_first_line(path: &str) -> Result<i32, Box<dyn Error>> {
let contents = fs::read_to_string(path)?; // std::io::Error
let first = contents.lines().next().unwrap_or("");
let number: i32 = first.trim().parse()?; // ParseIntError
Ok(number)
}
fn main() -> Result<(), Box<dyn Error>> {
match parse_first_line("number.txt") {
Ok(n) => println!("parsed: {}", n),
Err(e) => println!("error: {}", e),
}
Ok(())
}Box<dyn Error> is ideal for applications and scripts. For library code, prefer a custom error enum so callers can match on specific variants without downcasting.Dereferencing a Box
Box<T> implements the Deref trait, so you can use * to reach the inner value.
Rust also applies deref coercions automatically when calling methods or passing
references, so in practice you rarely need the explicit *.
fn print_length(s: &str) {
println!("length: {}", s.len());
}
fn main() {
let boxed = Box::new(String::from("hello, world"));
// Deref coercion: &Box<String> -> &String -> &str automatically
print_length(&boxed);
// Direct method call — coercion applies transparently
println!("uppercase: {}", boxed.to_uppercase());
// Explicit dereference to get a &String
let value: &String = &*boxed;
println!("value: {}", value);
}length: 12 uppercase: HELLO, WORLD value: hello, world
Box::leak — Creating &'static References
Box::leak consumes a Box<T> and returns a &'static mut T — a reference that
is valid for the entire program lifetime. The memory is intentionally never freed.
This is useful when you need a value to live for the entire program, such as global configuration built at startup.
fn build_config() -> &'static str {
let config = Box::new(String::from("production"));
Box::leak(config)
}
fn main() {
let cfg: &'static str = build_config();
println!("config: {}", cfg);
// 'cfg' is valid for the entire program lifetime
}Box::leak intentionally leaks memory — that memory is never reclaimed. Use it only when you genuinely need a &'static reference. For lazy globals, prefer std::sync::OnceLock or theonce_cell crate instead.Box is Zero-Overhead Beyond the Allocation
After the initial heap allocation there is no runtime overhead to using Box<T>. No
reference counting, no locking, no garbage collection. Accessing the inner value is
as fast as accessing it through any raw pointer.
Here is how Box compares to other common smart pointers:
Type | Ownership | Thread-safe | Mutable | Overhead |
|---|---|---|---|---|
Box<T> | Single owner | Yes (if T: Send) | Yes | One allocation only |
Rc<T> | Multiple owners | No | No (use with RefCell) | Reference count |
Arc<T> | Multiple owners | Yes | No (use with Mutex) | Atomic reference count |
&T / &mut T | Borrowed (no ownership) | Depends on T | Only with &mut | None |
When NOT to Use Box
Small, stack-sized data —
let x = 5;is faster and simpler thanBox::new(5).When ownership is clearly single and the data size is known at compile time — the stack is faster.
When you need shared ownership — use
Rc<T>(single thread) orArc<T>(multiple threads).When you need shared mutable access — combine
Rc<RefCell<T>>orArc<Mutex<T>>.
Box<T> is the right tool whenever you need heap allocation with single ownership: recursive types, large data transfers, and trait objects. It is simple, deterministic, and adds no hidden runtime costs beyond the allocation itself.