Rust 54 🦀 Smart Pointers: Box (Box) for Heap Allocation
Rust’s default allocation strategy is the stack. Local variables, function arguments, and fixed-size values live there, and their size must be known at compile time. Some values do not fit that model: a recursive type whose size depends on itself, a value too large to pass by copy, or a value that needs a stable address independent of the stack frame it was created in. For these cases, Rust provides Box<T>, a smart pointer that stores its value on the heap and keeps a fixed-size pointer on the stack. The Box itself is a single pointer’s worth of stack space, and the value it points to lives on the heap until the Box is dropped.
Key point: Box<T> is the simplest smart pointer. It owns its value, it allocates exactly once, and it has no overhead beyond the pointer itself. It exists to move a value to the heap, not to share it, not to count references to it, and not to mutate it through shared access. Those needs are met by other smart pointers.
Why Box exists
The recursive type problem. A struct that contains a field of its own type would have infinite size, because the field would contain another instance of the same struct, and so on. The compiler rejects the definition because it cannot compute the size. Wrapping the recursive field in Box<T> breaks the cycle: the struct contains a pointer, and pointers have a fixed size.
The large-value problem. A large struct passed by value is copied every time it is moved. If the struct is tens of kilobytes, the copies dominate the program’s cost. Storing it behind a Box reduces the move to a pointer copy, and the heap allocation happens once.
The stable-address problem. Some values must not move once they are created. A value that is referenced by a raw pointer, a value stored in a self-referential structure, or a value passed to an FFI function must stay at a fixed address. Box<T> allocates on the heap, and the heap address is stable until the Box is dropped.
The trait-object problem. A value of an unknown concrete type behind a trait object needs a place to live. Box<dyn Trait> stores the value on the heap and keeps a fat pointer to it on the stack. The fat pointer carries both the address and the vtable.
The enum-variant problem. An enum with a large variant and several small ones wastes memory on the small variants, because the enum is as large as its largest variant. Boxing the large variant reduces the enum’s size to a pointer.
The ownership problem. Box<T> has a single owner. When the owner goes out of scope, the value is dropped and the heap memory is freed. There is no reference counting, no shared ownership, and no runtime overhead beyond the single allocation.
a. Creating and using a Box
Box::new allocates on the heap and returns a Box<T>. The value inside is accessed with the dereference operator.
The basic construction
fn main() {
let b = Box::new(5);
println!("{}", *b); // 5
println!("{}", b); // 5, deref coercion
}
The Box owns the i32 on the heap. The stack holds a pointer. When b goes out of scope, the heap value is freed.
Deref coercion
Box<T> implements Deref<Target = T>. Method calls and field accesses on the box automatically dereference to the inner value.
fn main() {
let b = Box::new(String::from("hello"));
println!("{}", b.len()); // 5, deref to String
println!("{}", b.to_uppercase());
}
The b.len() call is (*b).len(). The Box is transparent for method resolution.
The Deref trait in function arguments
Deref coercion applies at coercion sites: function arguments and method receivers. A &Box<T> coerces to &T.
fn takes_str(s: &str) {
println!("{}", s);
}
fn main() {
let b = Box::new(String::from("hello"));
takes_str(&b); // &Box<String> coerces to &str
}
The compiler applies the chain &Box<String> → &String → &str.
Moving a Box
Moving a Box<T> moves the pointer, not the heap value. The value stays where it was allocated.
fn main() {
let a = Box::new(vec![1, 2, 3]);
let b = a; // The pointer is copied; the Vec does not move.
// a is no longer usable.
println!("{:?}", b);
}
The Vec’s elements are not copied. Only the pointer moves.
Dropping a Box
When a Box is dropped, the inner value is dropped first, then the heap memory is freed. This is the same behavior as any owning value.
fn main() {
let b = Box::new(String::from("hello"));
drop(b); // String is dropped, then the box's memory.
// println!("{}", b); // Error: b was dropped.
}
The order matters for types with custom Drop implementations.
Returning a Box from a function
A function can return a Box to transfer ownership of the heap value to the caller.
fn make_box() -> Box<i32> {
Box::new(42)
}
fn main() {
let b = make_box();
assert_eq!(*b, 42);
}
The allocation happens inside the function, and the ownership is returned.
b. The recursive type pattern
The most common reason to reach for Box is a type that refers to itself. The classic examples are a linked list and a tree.
The linked list
A linked list node contains a value and a reference to the next node. Without Box, the size is infinite.
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
The Box<List> makes the Cons variant a fixed size. The size of List is the size of i32 plus the size of a pointer, plus a discriminant.
The binary tree
A binary tree node contains a value and two references to its children.
struct Node {
value: i32,
left: Option<Box<Node>>,
right: Option<Box<Node>>,
}
fn main() {
let tree = Node {
value: 1,
left: Some(Box::new(Node { value: 2, left: None, right: None })),
right: Some(Box::new(Node { value: 3, left: None, right: None })),
};
}
The Option<Box<Node>> is the standard representation for a nullable child. None means no child.
The size calculation
The compiler computes the size of a recursive type by summing the fixed-size fields. The Box contributes the size of a pointer, breaking the infinite recursion.
use std::mem::size_of;
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
// On a 64-bit system: 8 bytes for i32 + 8 bytes for Box pointer.
println!("{}", size_of::<List>());
}
The enum is sized to accommodate the largest variant. The discriminant adds a few bytes.
Traversing a recursive type
A recursive function walks the structure by matching on the variants.
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, next) => value + sum(next),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
assert_eq!(sum(&list), 3);
}
The recursion terminates at Nil. The Box is transparent: next is a &Box<List>, and deref coercion reaches the inner List.
c. Box and trait objects
Box<dyn Trait> is the standard way to store a value of an unknown concrete type behind a trait interface. The Box owns the value, and the dyn Trait describes the interface.
The basic trait object
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) -> &str { "woof" }
}
impl Animal for Cat {
fn speak(&self) -> &str { "meow" }
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in &animals {
println!("{}", a.speak());
}
}
The Vec holds Box<dyn Animal>, which allows values of different concrete types in the same collection. Each Box owns one animal, and the dyn Animal carries the vtable.
The vtable and dynamic dispatch
Box<dyn Trait> is a fat pointer: it carries both the heap address and a pointer to the vtable. The vtable contains the function pointers for the trait’s methods.
let a: Box<dyn Animal> = Box::new(Dog);
// The fat pointer has two words:
// - address of the Dog on the heap
// - address of Dog's vtable
Calling a.speak() looks up the method in the vtable and calls it. This is dynamic dispatch, and it costs a pointer indirection compared to a generic call.
The function returning a trait object
A function can return a trait object when the concrete type is chosen at runtime.
fn make_animal(kind: &str) -> Box<dyn Animal> {
match kind {
"dog" => Box::new(Dog),
"cat" => Box::new(Cat),
_ => panic!("unknown animal"),
}
}
The return type is Box<dyn Animal>. The caller uses the trait interface without knowing the concrete type.
Box versus generics
A generic function with a trait bound is monomorphized: the compiler generates a separate version for each concrete type. A trait object erases the type and uses dynamic dispatch.
// Generic: static dispatch, one version per type.
fn speak_generic<T: Animal>(a: &T) {
println!("{}", a.speak());
}
// Trait object: dynamic dispatch, one version.
fn speak_dyn(a: &dyn Animal) {
println!("{}", a.speak());
}
The generic version is faster because the call is inlined. The trait object version is more flexible because the concrete type is not known at compile time.
Box
The standard library’s error trait is the most common trait object. Box<dyn std::error::Error> is the idiomatic way to return a boxed error from a function that can fail in multiple ways.
use std::error::Error;
fn might_fail() -> Result<i32, Box<dyn Error>> {
let n: i32 = "42".parse()?;
Ok(n)
}
The ? operator converts any error that implements Error into the boxed trait object. This is the pattern for functions that do not want to define a custom error type.
The Send and Sync bounds
A trait object can carry auto-trait bounds. Box<dyn Error + Send + Sync> is required for errors that cross thread boundaries.
fn thread_safe() -> Result<(), Box<dyn Error + Send + Sync>> {
Ok(())
}
The Send and Sync bounds are written explicitly because the compiler cannot infer them for a trait object.
Complete Example Session
// ============================================
// PART 1: BASIC BOX CONSTRUCTION
// ============================================
fn main() {
let b = Box::new(5);
assert_eq!(*b, 5);
assert_eq!(b.to_string(), "5");
}
// ============================================
// PART 2: DEREF COERCION
// ============================================
fn takes_str(s: &str) {
println!("{}", s);
}
fn main() {
let b = Box::new(String::from("hello"));
takes_str(&b); // &Box<String> → &str
}
// ============================================
// PART 3: MOVING A BOX
// ============================================
fn main() {
let a = Box::new(vec![1, 2, 3]);
let b = a; // Pointer moved, Vec stays on heap.
assert_eq!(b.len(), 3);
}
// ============================================
// PART 4: THE LINKED LIST
// ============================================
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, next) => value + sum(next),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
assert_eq!(sum(&list), 3);
}
// ============================================
// PART 5: THE BINARY TREE
// ============================================
struct Node {
value: i32,
left: Option<Box<Node>>,
right: Option<Box<Node>>,
}
fn main() {
let tree = Node {
value: 10,
left: Some(Box::new(Node { value: 5, left: None, right: None })),
right: Some(Box::new(Node { value: 15, left: None, right: None })),
};
assert_eq!(tree.left.as_ref().unwrap().value, 5);
}
// ============================================
// PART 6: TRAIT OBJECTS
// ============================================
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();
assert!(total > 7.0);
}
// ============================================
// PART 7: BOX<dyn ERROR>
// ============================================
use std::error::Error;
fn parse_int(s: &str) -> Result<i32, Box<dyn Error>> {
let n: i32 = s.parse()?;
Ok(n)
}
fn main() {
assert_eq!(parse_int("42").unwrap(), 42);
assert!(parse_int("abc").is_err());
}
// ============================================
// PART 8: BOXING A LARGE ENUM VARIANT
// ============================================
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
Large([u8; 1024]),
}
enum BoxedMessage {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
Large(Box<[u8; 1024]>),
}
fn main() {
// BoxedMessage is much smaller than Message.
// The large array lives on the heap.
}
// ============================================
// PART 9: BOX VS GENERICS
// ============================================
trait Speaker {
fn speak(&self) -> &str;
}
struct Dog;
impl Speaker for Dog {
fn speak(&self) -> &str { "woof" }
}
fn speak_generic<T: Speaker>(t: &T) -> &str {
t.speak()
}
fn speak_dyn(t: &dyn Speaker) -> &str {
t.speak()
}
fn main() {
let d = Dog;
assert_eq!(speak_generic(&d), "woof");
assert_eq!(speak_dyn(&d), "woof");
}
// ============================================
// PART 10: SIZE AND ALLOCATION
// ============================================
use std::mem::size_of;
fn main() {
// A Box pointer is one word.
assert_eq!(size_of::<Box<i32>>(), size_of::<usize>());
// A Box<dyn Trait> is two words: address and vtable.
assert_eq!(size_of::<Box<dyn std::fmt::Debug>>(), 2 * size_of::<usize>());
// The value itself lives on the heap.
let b = Box::new([0u8; 1024]);
assert_eq!(b.len(), 1024);
}
The ten parts progressed from basic construction through deref coercion, moving, the linked list, the binary tree, trait objects, Box<dyn Error>, boxing a large enum variant, static versus dynamic dispatch, and size considerations. The same allocation patterns ran through every part, showing where Box is the right tool.
Quick Reference
Box API
| Function | Purpose |
|---|---|
Box::new(value) | Allocate on the heap |
*b | Dereference to the inner value |
Box::into_raw(b) | Convert to a raw pointer |
Box::from_raw(p) | Reconstruct from a raw pointer |
Box::leak(b) | Get a &'static mut T, never freed |
b.downcast() | On Box<dyn Any>, recover the concrete type |
When to Use Box
| Scenario | Box Required |
|---|---|
| Recursive type | Yes |
| Trait object with ownership | Yes |
| Large value moved often | Yes |
| Stable address required | Yes |
| Large enum variant | Yes |
| Shared ownership | No, use Rc or Arc |
| Interior mutability | No, use RefCell or Mutex |
| Just borrowing | No, use &T |
Box Size
| Type | Size on 64-bit |
|---|---|
Box<i32> | 8 bytes |
Box<[i32; 100]> | 8 bytes |
Box<dyn Trait> | 16 bytes |
Box<[i32]> | 16 bytes (slice is a fat pointer) |
Box Versus Other Pointers
| Pointer | Ownership | Thread-safe | Overhead |
|---|---|---|---|
Box<T> | Unique | If T: Send | One allocation |
Rc<T> | Shared | No | Reference count |
Arc<T> | Shared | Yes | Atomic reference count |
&T | Borrowed | If T: Sync | None |
RefCell<T> | Interior mutability | No | Borrow flag |
Best Practices
✅ Do This:
// Use Box for recursive types.
enum List { Cons(i32, Box<List>), Nil } // ✅
// Use Box<dyn Trait> for heterogeneous collections.
let shapes: Vec<Box<dyn Shape>> = vec![...]; // ✅
// Use Box<dyn Error> for flexible error returns.
fn run() -> Result<(), Box<dyn Error>> { ... } // ✅
// Use Box to reduce enum size when a variant is large.
enum Msg { Small(i32), Large(Box<[u8; 1024]>) } // ✅
// Prefer generics over trait objects when the type is known.
fn speak<T: Speaker>(t: &T) -> &str { t.speak() } // ✅
// Add Send + Sync to trait objects that cross threads.
Box<dyn Error + Send + Sync> // ✅
❌ Don’t Do This:
// Don't box primitives for no reason.
let b = Box::new(5); // ❌ Unless required by an API
// Don't box just to avoid the borrow checker.
// Restructure the ownership instead. // ❌
// Don't use Box<dyn Trait> when a generic will do.
fn f(x: Box<dyn Display>) { } // ❌ Generic is faster
// Don't forget Send + Sync on thread-bound trait objects.
Box<dyn Error> // ❌ Not Send by default
// Don't manually free a Box.
// Drop handles it. // ❌
// Don't assume Box moves the inner value.
// Moving a Box moves the pointer only. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Recursive type won’t compile | Infinite size | Wrap the recursive field in Box |
Box<dyn Trait> not Send | Trait object lacks the bound | Write Box<dyn Trait + Send> |
| Box dropped too early | Ownership transferred | Keep the Box in scope |
Confused with Rc | Shared ownership needed | Use Rc<T> |
| Trait object cannot be cloned | dyn Trait is not Clone | Wrap in Rc or implement clone_box |
| Large enum still large | Boxed the wrong variant | Box the largest variant |
| Deref does not work | Missing Deref impl | Box provides it automatically |
| Stack overflow from deep recursion | Recursion depth too high | Consider an iterative approach |
Real-World Examples
1. Recursive Linked List
enum List { Cons(i32, Box<List>), Nil }
2. Binary Tree
struct Node { value: i32, left: Option<Box<Node>>, right: Option<Box<Node>> }
3. Trait Object Collection
let handlers: Vec<Box<dyn Fn()>> = vec![
Box::new(|| println!("a")),
Box::new(|| println!("b")),
];
4. Boxed Error
fn run() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
5. Boxing a Large Variant
enum Msg { Small(i32), Large(Box<[u8; 4096]>) }
6. Returning a Trait Object
fn make(kind: &str) -> Box<dyn Shape> { ... }
7. FFI with Stable Address
let b = Box::new(config);
let ptr = Box::into_raw(b);
8. Leaking for ‘static
let s: &'static str = Box::leak(Box::new("permanent"));
9. Box Downcast
use std::any::Any;
let b: Box<dyn Any> = Box::new(42);
let n = b.downcast::<i32>().unwrap();
10. Boxing a Closure
let f: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
Visual
Box on the Stack and Heap
┌────────────────────────────────────────────────────────────┐
│ Box<T>: STACK POINTER, HEAP VALUE │
│ │
│ STACK HEAP │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ let b = Box::new(5) │ │ │ │
│ │ │ │ │ │
│ │ b: ─────────────────┼─────▶│ 5 │ │
│ │ (8 bytes) │ │ (4 bytes) │ │
│ │ │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ The stack holds the pointer. The heap holds the value. │
│ Moving the Box moves the pointer, not the value. │
│ Dropping the Box frees the heap allocation. │
│ │
└────────────────────────────────────────────────────────────┘
The Recursive Type
┌────────────────────────────────────────────────────────────┐
│ WITHOUT BOX: INFINITE SIZE │
│ │
│ enum List { │
│ Cons(i32, List), ← size includes the size of List │
│ Nil, │
│ } │
│ │
│ List contains List contains List contains... │
│ The compiler rejects this: infinite size. │
│ │
│ WITH BOX: FIXED SIZE │
│ │
│ enum List { │
│ Cons(i32, Box<List>), ← size is i32 + pointer │
│ Nil, │
│ } │
│ │
│ Cons: [i32 | pointer] │
│ Nil: [] │
│ Largest variant: Cons. Enum size: i32 + pointer. │
│ │
└────────────────────────────────────────────────────────────┘
The Linked List in Memory
┌────────────────────────────────────────────────────────────┐
│ LINKED LIST IN HEAP MEMORY │
│ │
│ STACK │
│ ┌──────────────────────┐ │
│ │ list: ──────────────┼─┐ │
│ └──────────────────────┘ │ │
│ │ │
│ ▼ │
│ HEAP │
│ ┌─────────────────────────┐ │
│ │ Cons(1) │ │
│ │ value: 1 │ │
│ │ next: ─────────────────┼─┐ │
│ └─────────────────────────┘ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Cons(2) │ │
│ │ value: 2 │ │
│ │ next: ─────────────────┼─┐ │
│ └─────────────────────────┘ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Nil │ │
│ └─────────────────────────┘ │
│ │
│ Each node is a separate heap allocation. │
│ The stack holds only the pointer to the first node. │
│ │
└────────────────────────────────────────────────────────────┘
Box Fat Pointer
┌────────────────────────────────────────────────────────────┐
│ Box<dyn Trait>: FAT POINTER │
│ │
│ STACK HEAP │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ a: Box<dyn Animal> │ │ Dog │ │
│ │ ┌────────────────┐ │ │ │ │
│ │ │ data pointer ──┼──┼─────▶│ (Dog's fields) │ │
│ │ ├────────────────┤ │ │ │ │
│ │ │ vtable pointer─┼──┼─┐ └──────────────────────┘ │
│ │ └────────────────┘ │ │ │
│ └──────────────────────┘ │ │
│ │ │
│ ▼ │
│ VTABLE (static) │
│ ┌─────────────────────────┐ │
│ │ speak: fn(Dog) -> str │ │
│ │ drop: fn(Dog) │ │
│ │ size: size_of::<Dog> │ │
│ └─────────────────────────┘ │
│ │
│ Two words: address and vtable. │
│ Dynamic dispatch looks up the method in the vtable. │
│ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
Box<T> | Heap-allocated value with a single owner |
Box::new | Allocates on the heap |
*b | Dereferences to the inner value |
| Deref coercion | &Box<T> coerces to &T |
| Size | One pointer word, or two for Box<dyn Trait> |
| Drop | Frees the heap allocation |
| Recursive types | Require Box to break the size cycle |
Box<dyn Trait> | Trait object with ownership and a vtable |
Box<dyn Error> | Standard flexible error type |
Box::into_raw | Convert to a raw pointer |
Box::leak | Get a &'static mut T |
Key takeaways:
Box<T>moves a value to the heap. The stack holds a fixed-size pointer, and the value lives on the heap until theBoxis dropped. TheBoxowns the value; there is no sharing and no reference counting.- Recursive types require
Box. A struct or enum that contains itself has infinite size. Wrapping the recursive field inBoxreduces it to a pointer and makes the type’s size computable. - Moving a
Boxmoves the pointer, not the value. The heap allocation stays where it is. This makesBoxcheap to move even when the inner value is large. - Deref coercion makes
Boxtransparent. Method calls and field accesses automatically reach the inner value. Function arguments coerce from&Box<T>to&T. Box<dyn Trait>enables trait objects. TheBoxowns the value, and thedyn Traitcarries the vtable for dynamic dispatch. This is how heterogeneous collections and runtime-polymorphic returns are expressed.Box<dyn Error>is the idiomatic flexible error type. It allows a function to return any error that implementsError, without defining a custom enum. Add+ Send + Syncwhen the error crosses threads.- Boxing a large enum variant shrinks the enum. An enum is as large as its largest variant. Moving the large variant behind a
Boxreduces the enum’s size to a pointer. - Generics are usually faster than trait objects. A generic function is monomorphized and inlined. A trait object uses dynamic dispatch. Use trait objects when the concrete type is not known at compile time or when many types must coexist in one collection.
Boxhas no overhead beyond the allocation. It is not reference-counted, it does not track borrows, and it does not synchronize across threads. Those needs are met byRc,RefCell, andArc.
Remember: Box<T> is the simplest smart pointer in Rust. It exists to move a value to the heap, and it does exactly that with no additional machinery. It is the tool for recursive types, for large values that would be expensive to copy, for values that need a stable address, and for trait objects that own their concrete value. It is not the tool for sharing ownership, for interior mutability, or for thread-safe reference counting; those needs are met by other smart pointers. The Box owns its value, moves by pointer, derefs transparently, and frees the heap allocation when it is dropped. Once those four properties are understood, Box becomes the first tool to reach for when the stack does not suffice.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!