Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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::new and placement: the program builds the value on the stack, then moves it to the heap.
  • The Deref and DerefMut implementations that make a box transparent, and the deref-move (*b) that consumes the box. Box::into_inner stays nightly-only.
  • Boxing of recursive types: the indirection that gives an enum or a linked structure a finite size.
  • Boxing of trait objects: Box<dyn Trait> and Box<dyn Fn>, and thin pointers compared with fat pointers.
  • Deliberate exits from RAII. Box::leak is for values that live as long as the process. The Box::into_raw / Box::from_raw round trip is for API boundaries that pass raw pointers. Rust 1.99 adds the NonNull variants.

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 sized T is one word. A box of an unsized payload needs a second word of metadata: a length for Box<[T]>, or a vtable pointer for Box<dyn Trait>.
  • Niche optimization: a box is never null. Thus Option<Box<T>> uses the null bit pattern for None and 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:

  • *b reads or writes the payload.
  • Method calls auto-deref to the methods of T.
  • &Box<String> coerces to &str when 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:

  1. The pointer must come from Box::into_raw (same layout, same allocator).
  2. You must call Box::from_raw at most once for each into_raw. A second call is a double free.
  3. 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

ConceptKey point
Box::new(v)Allocates on the heap and moves v in. The program builds v on the stack first.
LayoutOne 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 / DerefMutThe box is transparent: *b, method calls with auto-deref, coercion to &T.
Deref-move *bConsumes the box and returns the payload. Only Box has this compiler support.
Box::into_innerNamed form of deref-move. It is still nightly (box_into_inner).
Recursive typesBox 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::leakExchanges the destructor for &'static mut T. Use it only for values that live as long as the process.
Box::into_raw / from_rawOwnership 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

FileDescription
07_01_box_fundamentals.rsBox::new, Deref/DerefMut, deref-move, thin vs. fat pointers, niche optimization
07_02_box_recursive_types.rsExpression tree and linked stack: Box gives recursive types a finite size
07_03_box_trait_objects.rsBox<dyn Trait> pipeline, factory function, Box<dyn Fn> closures
07_04_box_leak_raw.rsBox::leak for a &'static config, and the into_raw/from_raw round trip with safety notes
07_05_box_into_inner.rsBox::into_inner vs. deref-move (nightly, --features nightly)
07_15_box_non_null.rsBox::into_non_null / Box::from_non_null (1.99): ownership round trip with NonNull, and a two-node list with Option<NonNull<Node>> links