Using Box<T> to Store Data on the Heap
Understand how Rust's Box<T> smart pointer allocates values on the heap, why that matters for ownership and performance, and how to use it effectively in everyday code.
Box<T> is the simplest smart pointer in Rust. It owns a value that lives on the heap, while the Box itself remains on the stack as a plain pointer. When the Box goes out of scope, the heap memory is freed automatically — no manual cleanup, no garbage collector.
This single mechanism solves several practical problems that stack-only allocation cannot handle gracefully: transferring ownership of large data without expensive copies, representing types whose size is unknown at compile time, and storing trait objects. In this section we focus on the core mechanics of storing data on the heap with Box<T> — the foundation you need before exploring those more specialised cases.
Why Store Data on the Heap?
The stack is fast and predictable. Every local variable lives on the stack by default, and when a function returns, its stack frame is popped away, destroying those variables. The heap offers something different: a value allocated on the heap outlives any single function’s stack frame and stays in place until it is explicitly freed. A pointer to that value can be moved, copied, or passed around without ever relocating the data itself.
Box<T> gives you exactly that. The heap allocation remains at a fixed location while the pointer on the stack is cheap to move. This becomes immediately useful when a piece of data is large enough that moving its entire contents on every ownership transfer would hurt performance, or when you need a value to live for a caller-determined lifetime without resorting to complex lifetime annotations.
Stack vs. Heap Recap:
If the difference between stack and heap is fuzzy, revisit Chapter 4. In short: the stack is a linear, LIFO region with known sizes and no fragmentation; the heap is a larger pool where arbitrary-sized allocations are managed by the allocator. Stack allocation is cheaper but rigid; heap allocation is more flexible but involves bookkeeping.
Creating a Box and Accessing the Value Inside
The most basic use of Box<T> is to place a value on the heap. You create one with Box::new, and you access the inner value through the * dereference operator, just like you would with a reference.
fn main() {
let b = Box::new(5);
println!("b = {}", b); // prints: b = 5
let sum = *b + 10;
println!("sum = {}", sum); // prints: sum = 15
}
This looks almost indistinguishable from using a plain i32 on the stack, and that is intentional. Box<T> implements the Deref trait, so the compiler transparently turns *b and b.method() calls into accesses to the heap value. You do not need to write any explicit pointer arithmetic.
The integer 5 lives on the heap, allocated at runtime. The variable b is a pointer stored on the stack. When b goes out of scope at the end of main, Rust drops the Box, which deallocates the heap memory. You get the same ownership guarantees as a stack value, just with a level of indirection.
Don't Box Small, Frequent Values:
Putting a single integer on the heap is a toy demonstration. For small, copyable values that live and die within the same stack frame, stack allocation is both simpler and faster. Heap allocation involves calling the global allocator, which acquires a lock and manipulates internal data structures. Use Box only when the allocation solves a concrete problem.
Moving Ownership Without Copying the Data
A more realistic scenario is a large struct that you need to hand off to another function or store in a collection. Without a box, moving ownership means a byte-for-byte copy of the entire struct from one stack location to another. For a small struct that cost is negligible; for a struct containing a megabyte of data, it is not.
struct LargeReport {
data: [u8; 1_000_000],
title: String,
}
fn process_report(report: Box<LargeReport>) {
println!("Processing: {}", report.title);
// report goes out of scope here, heap freed
}
fn main() {
let report = Box::new(LargeReport {
data: [0; 1_000_000],
title: "Annual Summary".to_string(),
});
process_report(report); // moves the pointer, not the megabyte of data
}
What actually moves here is a single pointer — the Box<LargeReport> — which is the size of a machine word. The megabyte of report data stays exactly where it is on the heap. The function process_report takes ownership of the Box, and when it finishes, the Box is dropped and the heap allocation is freed.
This is the scenario where Box is the right tool: large data, clear ownership handoff, and no need for multiple owners (that would call for Rc or Arc). The technique is especially common when returning large results from functions without requiring the caller to provide a pre-allocated buffer.
Cheap Moves, Predictable Cleanup:
If you see a function that takes a Box<LargeStruct>, you know two things immediately: the caller can avoid copying the entire struct on the call, and the callee owns the data outright and will clean it up when finished. This pattern makes resource management explicit and efficient.
The Memory Layout
Visualising the layout helps internalise what Box actually does. For a Box<i32>, the picture is minimal:
Stack:
b: [ptr] ──────▶ Heap:
[5]
The stack contains only the pointer, whose size is always usize. The heap holds the actual i32. This same principle applies to any type T: Box<T> is a thin pointer (one word) to a T allocated on the heap, unless T is unsized, in which case the pointer may be wide (a data pointer plus a length or vtable pointer). We will encounter wide Box pointers with slices and trait objects later in the chapter.
How Box Frees Memory
Box<T> owns the heap allocation in the same sense that a String owns its buffer. When the Box is dropped — either by going out of scope or by calling drop — Rust invokes the destructor of the inner T (if it has one) and then deallocates the heap memory.
This is guaranteed at compile time. There is no runtime reference counting or garbage collection overhead; the lifetime of the allocation is tied directly to the variable that owns the Box. This is why Box is considered a single-owner smart pointer.
Stack Overflow with Large Stack Values in Debug Mode:
When you write Box::new([0u8; 10_000_000]), the array literal [0u8; 10_000_000] is first constructed on the stack and then moved into the heap allocation. In debug builds this intermediate stack value is not optimised away, so a sufficiently large array will overflow the stack. The final data still ends up on the heap, but the temporary allocation can crash your program. In release builds, the compiler eliminates the intermediate stack copy. If you must allocate a huge array in debug mode, use vec![0u8; N].into_boxed_slice() or construct the data in chunks.
Three Core Use Cases (and Where We Go Next)
The Rust Book identifies three primary situations where Box<T> is appropriate. Understanding them now gives you a map for the rest of this chapter:
- Types whose size cannot be known at compile time. The canonical example is a recursive type that contains itself — without indirection, the size would be infinite.
Boxprovides that indirection. This is the subject of the next section. - Large data where you want to transfer ownership without copying. We have already covered this with the
LargeReportexample. The same principle applies to returning large closures or async futures that you want to send across threads. - Owning a value solely for its trait, not its concrete type. Known as trait objects,
Box<dyn Trait>enables dynamic dispatch and heterogeneous collections. Chapter 18 will treat trait objects in depth.
The rest of this chapter builds on the foundation laid here. Whether you are dealing with recursive types, reference-counted sharing, or interior mutability, the underlying mechanism — a pointer on the stack to owned data on the heap — remains the same.
Common Misconceptions and Pitfalls
Beginners sometimes reach for Box in situations where simpler tools exist. Here are the mistakes you are most likely to encounter.
Using Box to “fix” borrowing errors. A compiler complaint about lifetimes or mutable aliasing does not mean “put this on the heap.” Box does not change borrowing rules; it only changes where the data lives. If a value must have a single owner, Box can make that explicit, but it does not enable multiple references or runtime borrow-checking — those require Rc or RefCell.
Confusing Box<T> with &T or &mut T. A reference borrows data that someone else owns; a Box owns the data it points to. A Box allocates and deallocates; a reference does not. When you see Box, think “I own this heap value,” not “I’m looking at someone else’s value.”
Boxing everything to avoid thinking about sizes. The stack is fast, and most Rust code never needs Box. If your type fits on the stack and you are not dealing with recursion or trait objects, let it live on the stack. Premature boxing adds allocator pressure and indirection without benefit.
Box Does Not Make Drops Faster:
Dropping a Box<LargeStruct> still drops the inner LargeStruct, which may involve running its Drop implementation and deallocating memory. The only thing Box avoids is the repeated memory copy that would happen with a stack move. The drop cost itself remains comparable.
Summary
Box<T> is the building block of heap-allocated ownership in Rust. It answers a straightforward need: keep the value on the heap, keep the pointer on the stack, and clean up automatically when the pointer disappears. From this simple idea, the language supports recursive data structures, efficient ownership transfer, and dynamic dispatch — all without a garbage collector.
The key insight to carry forward is that Box is a single-owner smart pointer. If you need shared ownership, you will need Rc or Arc; if you need runtime-checked mutation through a shared reference, you will need RefCell. Each of those types layers additional guarantees on top of the heap allocation model that Box establishes.