Box: values on the heap
A Box<T> owns one value that lives on the heap. The box itself is one machine word, and the value behind it has a fixed address until the box is dropped. That is the whole type: no counter, no lock, no clone on assignment.
Coming from C++? Smart pointers puts unique_ptr, shared_ptr and the Rust boxes in one program and shows which of them the compiler lets you copy.
This lesson uses a box for a large array, for a recursive enum, and for a trait object. The last section is the compiler error for a recursive type that forgot the indirection, which is the reason the box exists in the first place.
A value on the heap
Box::new moves its argument to a fresh allocation and returns the pointer. The value is still owned by one binding, so the rules of ownership do not change; only the address does. Dereferencing the box with * is what reads the value.
The program allocates a kilobyte of zeros and prints its length and the size of the box. The second print reads an integer through the box.
fn main() {
let buffer = Box::new([0u8; 1024]);
println!("{} {}", buffer.len(), std::mem::size_of_val(&buffer));
let value = Box::new(42);
println!("{}", *value + 1);
}
rustc --edition 2024 heap.rs
./heap
1024 8
43
The array is a thousand and twenty-four bytes on the heap and eight on the stack, which is the size of the pointer. buffer.len() works through auto-deref, the same rule the lesson on methods explains. A value that has to stay at one address while it is moved between owners is a box.
A recursive type
A type cannot contain itself by value: the size would be infinite. It can contain a box of itself, because a box has a fixed size whether the value behind it exists or not, and the recursion ends at a variant without a box.
The program builds a three-element list and sums it with a recursive function. The last print shows the size of one node, which is an integer and a pointer plus the tag the enum needs.
#[derive(Debug)]
enum List {
Node(i32, Box<List>),
End,
}
fn sum(list: &List) -> i32 {
match list {
List::Node(value, next) => value + sum(next),
List::End => 0,
}
}
fn main() {
let list = List::Node(1, Box::new(List::Node(2, Box::new(List::Node(3, Box::new(List::End))))));
println!("{}", sum(&list));
println!("{}", std::mem::size_of::<List>());
}
rustc --edition 2024 list.rs
./list
6
16
Sixteen bytes is the tag, the integer and the pointer, with the padding the layout chooses. The list is three allocations of that size, and dropping it walks the chain and frees each node. The lesson on enums with data builds the variant type this list uses.
A trait object in a box
A vector holds values of one type, so it cannot hold two shapes directly. It can hold two Box<dyn Shape> values, because each box is one pointer to a value whose type is erased behind the trait's table of methods.
The program puts a square and a circle in one vector and calls the same method on both. The output shows the two areas, and the dispatch happens at run time.
trait Shape {
fn area(&self) -> f64;
}
struct Square(f64);
struct Circle(f64);
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.0 * self.0 * self.0
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square(2.0)), Box::new(Circle(1.0))];
for shape in &shapes {
println!("{:.1}", shape.area());
}
}
rustc --edition 2024 dynbox.rs
./dynbox
4.0
3.0
The box holds the value, and the trait object is the pair of a data pointer and a table of function pointers. Box<dyn Shape> is the owning form of that pair, and &dyn Shape is the borrowing form. The lesson on trait objects explains the table, and the composition lesson shows when an enum is the better choice.
What a move of a box costs
Moving a box copies the pointer, not the value: the heap block stays where it is, and the old binding is gone. That is also why a box can be returned from a function cheaply, and why the address of the value does not change when the box changes hands.
The program builds a string on the heap inside a function, returns the box, and prints the length from the caller. The value never moves.
fn build() -> Box<String> {
Box::new(String::from("on the heap"))
}
fn main() {
let text = build();
let address = text.as_ptr();
println!("{}", text.len());
println!("{}", address == text.as_ptr());
}
rustc --edition 2024 build.rs
./build
11
true
Eleven bytes of text, and the pointer is the same before and after the print, which is the observable part of the promise. A Vec or a String moves the same way, so the box adds an allocation and removes the move of a large value. The ownership lesson explains why the old binding cannot be used after the move.
A recursive type without indirection
The enum below holds its own type by value in a variant. The compiler computes the size of the type and finds that the size depends on itself, which is E0072. The help line names the three ways to break the cycle: a box, an Rc or a reference.
The program never builds a node; the declaration alone is enough to stop the build.
enum List {
Node(i32, List),
End,
}
fn main() {
let list = List::End;
println!("{}", std::mem::size_of_val(&list));
}
rustc --edition 2024 recur_err.rs
error[E0072]: recursive type `List` has infinite size
--> recur_err.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Node(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Node(i32, Box<List>),
| ++++ +
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0072`.
The suggestion is a compilable line, which is rare enough to be worth reading: the compiler adds the box and closes the parenthesis. The same error appears for a struct that contains itself, and for a chain of two types that contain each other. The lesson on Rc and RefCell takes the other suggestion from the help line.
Exercises
- Build a binary tree of boxes and print its depth.
- Put three different types behind one trait in a Vec<Box<dyn Trait>> and call a method on each.
- Compare the size of a boxed array with the size of the array itself.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a
Box<T>own?Answer
One value on the heap
The box is the single owner of the allocation, with no counter.
-
Why does a recursive enum need a box?
Answer
A box has a fixed size, so the type can be sized
The indirection breaks the cycle of sizes that E0072 reports.
-
What is stored in a
Box<dyn Shape>?Answer
A pointer to the value and a table of its methods
The trait object is the data pointer plus the vtable, owned by the box.
-
What does moving a box copy?
Answer
The pointer, not the value
The heap block stays at its address and only the owner changes.
-
What does this program print?
fn main() { let value = Box::new(20); let other = value; println!("{}", *other + 22); }Answer
42
The box moved to other, and the new binding reads the same value.
-
What does this program print?
enum List { Node(i32, Box<List>), End, } fn main() { let list = List::Node(5, Box::new(List::End)); match list { List::Node(value, _) => println!("{value}"), List::End => println!("0"), } }Answer
5
The match binds the integer inside the node.
-
What does this program print?
fn main() { let array = Box::new([1u8, 2, 3, 4]); println!("{} {}", array.len(), std::mem::size_of_val(&array)); }Answer
4 8
The array is four bytes on the heap and the box is one pointer.
-
What does this program print?
trait Name { fn name(&self) -> &'static str; } struct Dog; impl Name for Dog { fn name(&self) -> &'static str { "rex" } } fn main() { let animal: Box<dyn Name> = Box::new(Dog); println!("{}", animal.name()); }Answer
rex
The call goes through the trait object to the implementation for Dog.
-
What is the first line of the compiler output for this program?
struct Node { value: i32, next: Node, } fn main() { let value = 5; println!("{value}"); }Answer
error[E0072]: recursive type
Nodehas infinite sizeA struct that contains itself by value has no finite size.
-
What is the first line of the compiler output for this program?
fn main() { let value = Box::new(7); println!("{}", value + 1); }Answer
error[E0369]: cannot add
{integer}toBox<{integer}>The box does not implement Add, so the value has to be dereferenced first.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev