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, movesvalue
into that memory, and returns aBox<T>
instance (which itself usually lives on the stack or in another structure). - Ownership: The
Box<T>
exclusively owns the heap-allocated data. Only oneBox<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, itsDrop
implementation is called, which deallocates the heap memory.
19.4.1 Key Features of Box<T>
- Exclusive Ownership: Ensures only one owner exists, aligning with Rust’s default ownership rules but for heap data.
- Heap Allocation: The primary way to explicitly put data on the heap in Rust.
- Known Size: A
Box<T>
always has the size of a pointer, regardless of the size ofT
. This is crucial for types whose size isn’t known at compile time. - Indirection: Provides a level of indirection.
Deref
andDerefMut
: Implements these traits, allowing aBox<T>
to be dereferenced using*
(e.g.,*my_box
) and enabling automatic deref coercions, so you can often call methods onT
directly via the box (e.g.,my_box.some_method()
).
19.4.2 Use Cases and Trade-Offs
Common Use Cases:
- 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, } }
- 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. - 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. - 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.