7.1 · Box<T>: The Simplest Heap Allocation
Domain 7 — Smart Pointers and Heap Allocation Duration: ~15 minutes Library components:
std::boxed::Box
Introduction
Box<T> is the simplest smart pointer in Rust. It is an owning pointer to one value on the heap. It has no reference count, no locks, and no runtime checks. It allocates when you create it, and it deallocates when it drops.
This simplicity is the reason why Box is very common. Use Box when you only need to put a value on the heap and own it. Each other smart pointer in this domain is easiest to understand as Box plus one more capability.
This tutorial shows:
Box::newand placement: the program builds the value on the stack, then moves it to the heap.- The
DerefandDerefMutimplementations that make a box transparent, and the deref-move (*b) that consumes the box.Box::into_innerstays nightly-only. - Boxing of recursive types: the indirection that gives an
enumor a linked structure a finite size. - Boxing of trait objects:
Box<dyn Trait>andBox<dyn Fn>, and thin pointers compared with fat pointers. - Deliberate exits from RAII.
Box::leakis for values that live as long as the process. TheBox::into_raw/Box::from_rawround trip is for API boundaries that pass raw pointers. Rust 1.99 adds theNonNullvariants.
Box::new and Placement Semantics
Box::new(v) allocates space on the heap and moves v into that space. The sequence is important. First, the program evaluates the expression that makes v, on the stack. Then the program copies the completed value to the heap. Rust does not guarantee in-place construction (placement new). For very large values, the temporary copy on the stack is real, and it is occasionally important.
After construction, the box itself is one machine word on the stack: the pointer. The ownership semantics are the same as for any other value. When the Box goes out of scope, its Drop implementation frees the heap allocation. A move of the Box moves only the pointer, never the payload.
Figure: Box<T> — Stack Handle, Heap Payload
// The 512-byte array goes to the heap. `on_heap` is the pointer on the stack.
let on_heap: Box<[u8; 512]> = Box::new([0u8; 512]);
assert_eq!(size_of::<Box<[u8; 512]>>(), size_of::<usize>()); // handle: 1 word
let big = Box::new([7u8; 4096]); // Box<[u8; 4096]>
let payload_addr: *const u8 = big.as_ptr(); // address of the payload on the heap
let moved = big; // moves 8 bytes, not 4096
assert!(std::ptr::eq(payload_addr, moved.as_ptr())); // the payload did not move
Remember these two facts about size:
- Thin vs. fat:
Box<T>for a sizedTis one word. A box of an unsized payload needs a second word of metadata: a length forBox<[T]>, or a vtable pointer forBox<dyn Trait>. - Niche optimization: a box is never null. Thus
Option<Box<T>>uses the null bit pattern forNoneand has the size of one pointer. For this reason,Option<Box<Node>>is the idiomatic nullable owning pointer in Rust.
use std::fmt::Display;
let word = size_of::<usize>(); // 8 bytes on a 64-bit target
assert_eq!(size_of::<Box<i32>>(), word); // thin
assert_eq!(size_of::<Box<[i32]>>(), 2 * word); // ptr + len
assert_eq!(size_of::<Box<dyn Display>>(), 2 * word); // ptr + vtable
assert_eq!(size_of::<Option<Box<i32>>>(), word); // niche: null = None
Deref, DerefMut, and Moving Out
Box<T> implements Deref<Target = T> and DerefMut, so a box is transparent in almost every position:
*breads or writes the payload.- Method calls auto-deref to the methods of
T. &Box<String>coerces to&strwhen a function needs a&str.
The same mechanism gives Vec<T> the slice methods (tutorial 4.1).
let mut counter = Box::new(41_i32);
*counter += 1; // DerefMut: writes through the box
assert_eq!(*counter, 42); // Deref: reads through the box
let message = Box::new(String::from("hello, heap")); // Box<String>
assert_eq!(message.len(), 11); // String::len via auto-deref
fn shout(s: &str) -> String { s.to_uppercase() }
// Deref coercion: &Box<String> → &String → &str
assert_eq!(shout(&message), "HELLO, HEAP");
Deref-move: *b in value position
Box is the only smart pointer that lets you move the payload out through a dereference. let v = *b; consumes the box, frees the heap allocation, and gives you the payload by value. The compiler has built-in support for this operation, and the Deref trait cannot express it. For this reason, a move through *rc on an Rc does not compile.
let boxed_config = Box::new(String::from("mode=fast")); // Box<String>
let config: String = *boxed_config; // deref-move: consumes boxed_config
assert_eq!(config, "mode=fast");
// A use of boxed_config here is error[E0382]: the box no longer exists.
The explicit, named form is Box::into_inner(b). It is still nightly-only (feature box_into_inner, tracking issue #80437). It exists because a named function is clearer in generic code, and because *b is easy to read incorrectly as a plain deref. Example 07_05_box_into_inner.rs shows it behind the nightly feature gate of this repository.
07_01_box_fundamentals.rs prints (the sizes are for a 64-bit target):
size_of::<[u8; 512]>() = 512 bytes (the payload)
size_of::<Box<[u8; 512]>>() = 8 bytes (the handle)
Box<i32> = 8 bytes (thin)
Box<[i32]> = 16 bytes (ptr + len)
Box<dyn Display> = 16 bytes (ptr + vtable)
Option<Box<i32>> = 8 bytes (niche: null means None)
shout(&message) = "HELLO, HEAP"
payload stayed put across the move: true
moved out of the box: "mode=fast"
All assertions passed.
Boxing Recursive Types
The compiler cannot give a layout to a type that contains itself directly, because the size would be infinite:
// This enum does not compile.
enum Expr {
Num(i64),
Add(Expr, Expr), // error[E0072]: recursive type `Expr` has infinite size
}
The suggestion of the compiler is the solution: insert some indirection. A Box<Expr> is always one word, whatever it points to. Thus each variant has a fixed size, and the compiler can give the enum a layout. The recursion then occurs on the heap.
Figure: Expression Tree — Every Edge Is a Box
enum Expr {
Num(i64),
Add(Box<Expr>, Box<Expr>), // each child is one pointer to a node on the heap
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}
fn eval(expr: &Expr) -> i64 {
match expr {
Expr::Num(n) => *n,
// `a` and `b` are &Box<Expr>. Deref coercion passes them to eval as &Expr.
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Sub(a, b) => eval(a) - eval(b),
Expr::Mul(a, b) => eval(a) * eval(b),
Expr::Neg(e) => -eval(e),
}
}
// num, add, sub, mul, and neg are helper functions. Each one returns a Box<Expr>.
// For example: fn num(n: i64) -> Box<Expr> { Box::new(Expr::Num(n)) }
// The tree for (2 + 3) * -(4 - 6):
let expr = mul(add(num(2), num(3)), neg(sub(num(4), num(6))));
assert_eq!(eval(&expr), 10); // 5 * 2
The evaluation goes through the boxes with plain & borrows. A match on &Expr gives a reference to each boxed child, and that reference auto-derefs to &Expr.
The same indirection also makes linked structures possible. With Option<Box<Node>> as the next pointer, you can make a singly linked stack. Its push and pop operations are an Option::take and a new link.
07_02_box_recursive_types.rs prints:
expr = ((2 + 3) * -(4 - 6))
value = 10
size_of::<Expr>() = 24 bytes
stack popped in LIFO order: 30, 20, 10
All assertions passed.
Box<dyn Trait>: Owned Trait Objects
dyn Trait is unsized, because different implementors have different sizes. Thus a dyn Trait value must be behind a pointer. Box<dyn Trait> is the owning pointer: a fat pointer of two words (data pointer and vtable pointer). With it, you can store values of different types together, and you can select those types at runtime.
trait Stage {
fn name(&self) -> &'static str;
fn apply(&self, input: String) -> String;
}
// Trim, Redact, and Uppercase are three structs that implement Stage.
// A factory: the `spec` string selects the concrete type at runtime.
fn stage_for(spec: &str) -> Option<Box<dyn Stage>> {
match spec {
"trim" => Some(Box::new(Trim)), // Box<Trim> coerces to Box<dyn Stage>
"redact" => Some(Box::new(Redact { secret: "1234" })),
"upper" => Some(Box::new(Uppercase)),
_ => None, // an unknown name gives no stage
}
}
// A heterogeneous pipeline: three different types, one element type.
// specs: [&str; 3] = ["trim", "redact", "upper"]
let pipeline: Vec<Box<dyn Stage>> =
specs.iter().filter_map(|spec| stage_for(spec)).collect();
// text: String, starts as " the launch code is 1234 "
for stage in &pipeline {
text = stage.apply(text); // dynamic dispatch through the vtable
}
// text is now "THE LAUNCH CODE IS [REDACTED]"
Each call goes through the vtable at runtime. That indirection is the cost of dynamic dispatch. Type erasure is its benefit.
The same technique applies to closures, which have unique types that you cannot name. Box<dyn Fn(i64) -> i64> lets you store closures in fields and collections. Box<dyn Error> (tutorial 2.2) applies this pattern to error handling. Domain 24 examines trait objects in full.
07_03_box_trait_objects.rs prints:
input: " the launch code is 1234 "
after trim: "the launch code is 1234"
after redact: "the launch code is [REDACTED]"
after upper: "THE LAUNCH CODE IS [REDACTED]"
size_of::<Box<dyn Stage>>() = 16 bytes (ptr + vtable)
start: 3
after double: 6
after square: 36
after negate: -36
All assertions passed.
Box::leak and the Raw Pointer Round Trip
Sometimes you must deliberately go out of RAII. Box gives two ways to do this. Each one transfers the responsibility for the allocation, not the allocation itself.
Box::leak — a deliberate, one-time leak
Box::leak(b) consumes the box and returns &'static mut T. The destructor never runs, so the reference is valid for the remainder of the program. This is the standard method to make a &'static reference from a value that the program computes at runtime. For example, CLI tools leak their parsed arguments, and loggers leak their configuration. The OS reclaims the memory when the process exits.
General rule: leak only values that must live as long as the process. Box::leak in a loop is an ordinary memory leak, which is a defect.
fn make_static_config(region: &str, verbose: bool) -> &'static str {
let config = format!("region={region};verbose={verbose}"); // a String made at runtime
// String → Box<str> → &'static mut str, which coerces to the &'static str return type.
Box::leak(config.into_boxed_str())
}
// make_static_config("eu-west-1", true) returns "region=eu-west-1;verbose=true".
Box::into_raw / Box::from_raw — ownership as a raw pointer
Box::into_raw(b) disables the destructor and gives you the raw pointer to the allocation. Box::from_raw(ptr) is the inverse: it attaches ownership and the destructor again. With this pair, owned values go across API boundaries that pass raw pointers. C FFI handles are the primary example (domain 20).
The safety contract of the round trip:
- The pointer must come from
Box::into_raw(same layout, same allocator). - You must call
Box::from_rawat most once for eachinto_raw. A second call is a double free. - Between the two calls, no other code may free the pointer. No reference into the allocation may stay alive across the reconstruction.
struct Session { user: String, hits: u32 }
let session = Box::new(Session { user: String::from("ada"), hits: 0 });
let raw: *mut Session = Box::into_raw(session); // ownership → raw pointer
// No Box owns the allocation now. This code is responsible for it.
// SAFETY: `raw` comes from Box::into_raw, and no code freed it. No other
// pointer or reference to the allocation exists.
unsafe {
(*raw).hits += 1;
(*raw).hits += 1;
}
// SAFETY: `raw` comes from Box::into_raw. This is the only from_raw call,
// and no code uses `raw` after it.
let session: Box<Session> = unsafe { Box::from_raw(raw) };
assert_eq!(session.hits, 2); // `session` drops normally: no leak, no double free
07_04_box_leak_raw.rs prints:
static config: region=eu-west-1;verbose=true
session round-tripped through a raw pointer: user=ada, hits=2
All assertions passed.
Rust 1.99 adds the same round trip with a NonNull<T> pointer: Box::into_non_null and Box::from_non_null. A Box pointer is never null. *mut T does not record that fact, and NonNull<T> does. The safety contract is the same as for into_raw / from_raw.
Use this pair when you store the pointer. For example, a list can store each link as an Option<NonNull<Node>>, which is one pointer wide (tutorial 16.3):
use std::ptr::NonNull;
let boxed: Box<String> = Box::new(String::from("heap value"));
let ptr: NonNull<String> = Box::into_non_null(boxed); // ownership → NonNull pointer
// No Box owns the allocation now. The String stays alive.
// SAFETY: `ptr` comes from Box::into_non_null, and this is the only conversion back.
let restored: Box<String> = unsafe { Box::from_non_null(ptr) };
assert_eq!(*restored, "heap value"); // `restored` drops normally
07_15_box_non_null.rs also makes a two-node list with these functions, and then frees it. It prints:
round trip: heap value
Option<NonNull<Node>> = 8 bytes, *mut Node = 8 bytes
sum of the linked nodes: 3
freed 2 nodes
All assertions passed.
Summary
| Concept | Key point |
|---|---|
Box::new(v) | Allocates on the heap and moves v in. The program builds v on the stack first. |
| Layout | One word (thin pointer) for a sized T. Two words for Box<[T]> and Box<dyn Trait>. |
Option<Box<T>> | Has the size of one pointer because of the niche optimization. It is the idiomatic nullable owning pointer. |
Deref / DerefMut | The box is transparent: *b, method calls with auto-deref, coercion to &T. |
Deref-move *b | Consumes the box and returns the payload. Only Box has this compiler support. |
Box::into_inner | Named form of deref-move. It is still nightly (box_into_inner). |
| Recursive types | Box gives recursive enums and linked structures a finite size. |
Box<dyn Trait> | Owned trait object. The fat pointer is a data pointer and a vtable pointer. Dispatch is at runtime. |
Box<dyn Fn…> | Erases closure types that you cannot name. You can then store closures in fields and collections. |
Box::leak | Exchanges the destructor for &'static mut T. Use it only for values that live as long as the process. |
Box::into_raw / from_raw | Ownership across API boundaries that pass raw pointers. Call from_raw exactly one time for each into_raw. |
Box::into_non_null / from_non_null (1.99) | The same round trip with NonNull<T>. The type records that the pointer is not null. |
Code Examples
| File | Description |
|---|---|
07_01_box_fundamentals.rs | Box::new, Deref/DerefMut, deref-move, thin vs. fat pointers, niche optimization |
07_02_box_recursive_types.rs | Expression tree and linked stack: Box gives recursive types a finite size |
07_03_box_trait_objects.rs | Box<dyn Trait> pipeline, factory function, Box<dyn Fn> closures |
07_04_box_leak_raw.rs | Box::leak for a &'static config, and the into_raw/from_raw round trip with safety notes |
07_05_box_into_inner.rs | Box::into_inner vs. deref-move (nightly, --features nightly) |
07_15_box_non_null.rs | Box::into_non_null / Box::from_non_null (1.99): ownership round trip with NonNull, and a two-node list with Option<NonNull<Node>> links |