21.2Box<T>
Box<T> is the simplest smart pointer, and the one to learn first. It does exactly one thing: it puts a value of type T on the heap instead of the stack, and gives you an owning handle to it on the stack. That's it. A Box<T> owns its heap value, frees it on drop, and otherwise behaves like the value it holds. Most of the time you won't reach for it, stack values are cheaper and the default, but in a few situations it's exactly the tool, and one of them is unavoidable.
Boxing a value
Here's a box holding an i32:
fn main() {
let b = Box::new(5);
println!("b = {b}");
}b = 5
Box::new(5) allocates space for an i32 on the heap, stores 5 there, and returns a Box<i32>, a stack handle pointing at it. When b goes out of scope at the end of main, the box is dropped, which frees the heap allocation. Printing b prints 5, because a box transparently stands in for its contents (that's Deref, next lesson).
Boxing a lone integer is pointless, an i32 is perfectly happy on the stack, and this example exists only to show the mechanics. So when would you box something? Three cases, and the third is the real reason Box exists.
You might box a large value to avoid copying it around when it moves: moving a Box copies only the small handle, not the big payload. You might box a value to store it as a trait object, Box<dyn Trait> (lesson 16.9), where the concrete type's size isn't known. And you must box to build a recursive type, which is where we'll spend the rest of the lesson, because it's the case the compiler forces on you.
The recursive type problem
Suppose you want a classic linked list: each element holds a value and the rest of the list. Following a long tradition, we'll call it a cons list (the name comes from Lisp's cons function). A natural first attempt with an enum (chapter 11):
enum List {
Cons(i32, List),
Nil,
}
Cons holds an i32 and the rest of the list; Nil marks the end. It reads perfectly. It also doesn't compile, and the error is the whole point of this lesson:
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
Read it carefully, because the compiler is teaching the concept. To lay out a List on the stack, the compiler needs its size. A List is a Cons, which contains a List, which is a Cons, which contains a List... The size is "the size of a List, plus a bit," an equation with no finite answer. The type has infinite size, and the compiler can't allocate it. And look at the help text: it doesn't just report the error, it names the fix and even shows the edit. "Insert some indirection (e.g., a Box, Rc, or &)." This is rustc at its best, and learning to act on messages like this is the skill the whole course has been building.
Box breaks the cycle
The fix the compiler suggested: put the recursive part behind a Box. A Box<List> is a pointer, and a pointer has a fixed, known size (one machine word) no matter how big the thing it points to is. The size equation now terminates:
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
let mut current = &list;
while let Cons(value, next) = current {
println!("{value}");
current = next;
}
}1
2
3
Now a Cons holds an i32 and a Box<List>. The Box is a fixed-size pointer to the next List on the heap, so a List's size is "an i32 plus a pointer", finite and knowable. The list 1 -> 2 -> 3 -> Nil builds, each Box::new heap-allocating the rest. The while let (chapter 11) walks it by following each next pointer until it hits Nil. We've built a recursive data structure, and Box is the single ingredient that made it possible.
Key insight
A value's size must be known at compile time to live on the stack (the chapter-8 rule). A recursive type that directly contains itself has no finite size. A pointer, by contrast, is always one word wide regardless of what it points to. So Box<T> turns an unsizeable "a List contains a List" into a sizeable "a List contains a pointer to a List." Every recursive structure in Rust, lists, trees, graphs, is built on this single move: replace the recursive field with a pointer to it.
Box owns, and frees
A Box is a smart pointer, so it owns its contents and cleans up automatically. When list is dropped at the end of main, its drop frees its boxed List, whose drop frees the next box, all the way down the chain, no manual deallocation, no leak (the chapter-8 ownership rules, applied through the boxes). You allocated explicitly with Box::new, but you never free explicitly; ownership handles it. That's the smart-pointer promise: heap allocation with the convenience and safety of a stack value.
For advanced readers
A cons list isn't actually how you'd store a sequence in Rust, a Vec<i32> (chapter 18) is faster and simpler, because its elements sit contiguously rather than scattered across the heap with a pointer-chase between each. The cons list is a teaching example for recursion and Box, not a recommendation. Where boxed recursion really earns its place is in genuinely tree-shaped or graph-shaped data: a syntax tree, a file-system hierarchy, an expression evaluator. For a plain list, reach for Vec.
Quiz time
Question #1
What does Box::new(value) do, and what happens to the heap allocation when the box goes out of scope?
Show solution
Box::new(value) allocates space on the heap, moves value into it, and returns a Box<T>, a fixed-size owning handle on the stack pointing to the heap data. When the box goes out of scope it's dropped, which frees the heap allocation automatically. You allocate explicitly but never free explicitly; ownership handles deallocation.
Question #2
Why does enum List { Cons(i32, List), Nil } fail to compile, and how does Box fix it?
Show solution
It has "infinite size" (E0072): computing a List's size requires knowing a List's size, since Cons directly contains a List, an equation with no finite answer, so the compiler can't lay it out. Wrapping the recursive field in Box (Cons(i32, Box<List>)) replaces the inline List with a pointer to a heap List. A pointer has a fixed, known size, so the type becomes sizeable. (The compiler's error even suggests this fix.)
Question #3
Besides recursive types, name one other reason to use Box<T>.
Show solution
Either: to store a value as a trait object (Box<dyn Trait>, lesson 16.9) when the concrete type's size isn't known; or to avoid copying a large value when it moves (moving a Box copies only the small pointer, not the big payload). Recursive types are the case the compiler forces; these are cases where boxing is a deliberate choice.
A box stands in transparently for its contents, you printed and walked through one as if it weren't there. That transparency is the Deref trait, and the next lesson (21.3) opens it up, finally solving the &String-works-as-&str mystery from chapter 9.