Rust When to Box
A Box holds a value on the heap behind a small pointer. Boxing trades one allocation for a handle that is always 8 bytes.
Situation: the type contains itself
No Box, no size. The Box breaks the endless loop, as Rust Box showed:
enum List { Cons(i32, Box<List>), Nil }Situation: the kinds must mix
Different types become one item type behind the fat pointer. Rust Trait Objects tells the full story:
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];Situation: the value is too big for a frame
A 4 KB array is a heavy local value. Behind a Box, the local part is one pointer:
let page = Box::new([0u8; 4096]);
println!("handle = {} bytes", std::mem::size_of::<Box<[u8; 4096]>>());The trap: Box::new still starts on the stack
Box::new receives a finished value. A huge array is finished on the stack first. On the machine that tested this lesson, this line overflowed the stack before the Box ever existed:
let huge = Box::new([7u8; 16_000_000]); // the array is built on the stack firstBuild big data on the heap instead
vec! grows on the heap from the very first byte. into_boxed_slice then freezes the finished data into a boxed slice:
let data: Box<[u8]> = vec![7u8; 16_000_000].into_boxed_slice();
println!("len = {}", data.len());Example
enum Crate { Empty, Full(Box<[u8; 512]>) }
fn weigh(c: &Crate) -> &'static str {
match c { Crate::Empty => "empty", Crate::Full(_) => "full" }
}
fn main() {
println!("one crate = {} bytes", std::mem::size_of::<Crate>());
let crates = vec![Crate::Empty, Crate::Full(Box::new([9u8; 512]))];
for c in &crates { println!("{}", weigh(c)); }
let note: &'static str = Box::leak(String::from("fragile").into_boxed_str());
println!("note for the whole run: {note}");
}Key points
- Boxing moves a value to the heap and leaves a small pointer behind.
- Ask two questions: must the data live on the heap?
- Must the handle stay small and cheap to move?
- Two yeses point to Box
- Box a type that contains itself: the pointer gives the whole type a known size
- Box the fat variant of an enum: the enum then weighs a pointer, not the payload
- Do not box small values by habit: a plain value needs no allocation and no pointer chase
- Box::new cannot rescue a value that is first built on the stack: build big data on the heap instead
- You pay one allocation.
- You gain a handle that is always 8 bytes.
- Reach for a Box only for a handful of honest reasons, and avoid boxing small values by habit.
In this example, Crate is an enum with two variants. Full carries a Box, so the whole enum weighs 8 bytes. A Box pointer is never null. So the enum borrows the null address to mean Empty. No tag bytes are added. The vector holds one crate of each kind, and weigh reads the variant back. The last lines show the escape hatch. Box::leak freezes a String into a &'static str that lives until the programme ends. Nothing frees it, and nothing needs to.
Note
Boxing is a trade, not an upgrade.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A struct holds a [u8; 9000] field. The struct is passed around by value all day. What is the cheapest first move?