19.4 Box<T>: Simple Heap Allocation

Box<T> is the most basic smart pointer, providing ownership of data allocated on the heap.

  • Creation: Box::new(value) allocates memory on the heap, moves value into that memory, and returns a Box<T> instance (which itself usually lives on the stack or in another structure).
  • Ownership: The Box<T> exclusively owns the heap-allocated data. Only one Box<T> points to a given allocation at a time (though ownership can be transferred via moves).
  • Deallocation: When the Box<T> goes out of scope, its Drop implementation is called, which deallocates the heap memory.

19.4.1 Key Features of Box<T>

  1. Exclusive Ownership: Ensures only one owner exists, aligning with Rust’s default ownership rules but for heap data.
  2. Heap Allocation: The primary way to explicitly put data on the heap in Rust.
  3. Known Size: A Box<T> always has the size of a pointer, regardless of the size of T. This is crucial for types whose size isn’t known at compile time.
  4. Indirection: Provides a level of indirection.
  5. Deref and DerefMut: Implements these traits, allowing a Box<T> to be dereferenced using * (e.g., *my_box) and enabling automatic deref coercions, so you can often call methods on T directly via the box (e.g., my_box.some_method()).

19.4.2 Use Cases and Trade-Offs

Common Use Cases:

  1. Recursive Data Structures: To define types that need to contain pointers to themselves (e.g., nodes in a list or tree), Box<T> breaks the infinite size calculation at compile time by providing indirection.
    #![allow(unused)]
    fn main() {
    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
    }
  2. Trait Objects: To store an object implementing a specific trait when the concrete type isn’t known at compile time (dyn Trait). Box<dyn Trait> provides the necessary indirection and owns the unknown-sized object on the heap.
  3. Transferring Large Data: Moving a Box<T> only copies the pointer (stack size), not the potentially large heap data, which can be more efficient than moving the large data structure itself.
  4. Explicit Heap Placement: To avoid placing large data structures on the stack, preventing potential stack overflows, especially in constrained environments or deep recursion.

Trade-Offs:

  • Indirection Cost: Accessing heap data via a pointer involves an extra memory lookup compared to direct stack access, potentially leading to cache misses and a small performance penalty.
  • Allocation Cost: Heap allocation and deallocation operations are generally slower than stack allocation.

Example:

fn main() {
    let stack_val = 5; // On the stack

    // Allocate an integer on the heap
    let boxed_val: Box<i32> = Box::new(stack_val);

    // Access the value using dereferencing
    println!("Value on heap: {}", *boxed_val);
    // Methods can often be called directly due to Deref coercion
    println!("Heap value + 10: {}", boxed_val.checked_add(10).unwrap_or(0));

    // `boxed_val` goes out of scope here. Its Drop implementation runs,
    // freeing the heap memory.
}

Note: For specific advanced scenarios, particularly involving async code or FFI where data must not be moved in memory after allocation, Pin<Box<T>> is used. This provides guarantees about memory location stability.