| |

Rust 54 🦀 Smart Pointers: Box (Box) for Heap Allocation

⏱️ Estimated read: 19 min | |

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

FunctionPurpose
Box::new(value)Allocate on the heap
*bDereference 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

ScenarioBox Required
Recursive typeYes
Trait object with ownershipYes
Large value moved oftenYes
Stable address requiredYes
Large enum variantYes
Shared ownershipNo, use Rc or Arc
Interior mutabilityNo, use RefCell or Mutex
Just borrowingNo, use &T

Box Size

TypeSize 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

PointerOwnershipThread-safeOverhead
Box<T>UniqueIf T: SendOne allocation
Rc<T>SharedNoReference count
Arc<T>SharedYesAtomic reference count
&TBorrowedIf T: SyncNone
RefCell<T>Interior mutabilityNoBorrow 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

PitfallWhy It HappensFix
Recursive type won’t compileInfinite sizeWrap the recursive field in Box
Box<dyn Trait> not SendTrait object lacks the boundWrite Box<dyn Trait + Send>
Box dropped too earlyOwnership transferredKeep the Box in scope
Confused with RcShared ownership neededUse Rc<T>
Trait object cannot be cloneddyn Trait is not CloneWrap in Rc or implement clone_box
Large enum still largeBoxed the wrong variantBox the largest variant
Deref does not workMissing Deref implBox provides it automatically
Stack overflow from deep recursionRecursion depth too highConsider 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

ItemValue
Box<T>Heap-allocated value with a single owner
Box::newAllocates on the heap
*bDereferences to the inner value
Deref coercion&Box<T> coerces to &T
SizeOne pointer word, or two for Box<dyn Trait>
DropFrees the heap allocation
Recursive typesRequire 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_rawConvert to a raw pointer
Box::leakGet 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 the Box is dropped. The Box owns 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 in Box reduces it to a pointer and makes the type’s size computable.
  • Moving a Box moves the pointer, not the value. The heap allocation stays where it is. This makes Box cheap to move even when the inner value is large.
  • Deref coercion makes Box transparent. Method calls and field accesses automatically reach the inner value. Function arguments coerce from &Box<T> to &T.
  • Box<dyn Trait> enables trait objects. The Box owns the value, and the dyn Trait carries 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 implements Error, without defining a custom enum. Add + Send + Sync when 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 Box reduces 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.
  • Box has 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 by Rc, RefCell, and Arc.

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.


Was this helpful?


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!