Heap Allocation with Box
Heap allocation with Box<T> transfers values from fixed-size thread stack frames into the process heap, reducing stack frame pressure, making ownership transfers constant-time (8-byte pointer moves), and providing owned storage for unsized slices (Box<[T]>, Box<str>) and trait objects (Box<dyn Trait>).
Avoiding Expensive Stack Copies on Move
Moving a value in Rust performs a bitwise shallow copy (memcpy) of its stack representation. For a 24-byte Vec<T> or String, moving is already cheap because only the 24-byte header is copied. For a large fixed-size array ([u8; 65_536]) or a large struct, moving copies all 65,536 bytes on every transfer and risks overflowing the thread stack. Placing the value inside a Box<T> reduces every subsequent move to copying an 8-byte pointer.
struct AudioFrame {
samples: [f32; 4096],
}
fn process_frame(mut frame: Box<AudioFrame>) -> Box<AudioFrame> {
frame.samples[0] = 1.0;
frame.samples[4095] = -1.0;
frame
}
fn main() {
let frame = Box::new(AudioFrame {
samples: [0.0; 4096],
});
println!(
"stack handle size: {} bytes, heap payload size: {} bytes",
size_of_val(&frame),
size_of_val(&*frame)
);
let processed = process_frame(frame);
println!(
"endpoints: ({}, {})",
processed.samples[0], processed.samples[4095]
);
}
stack handle size: 8 bytes, heap payload size: 16384 bytes
endpoints: (1, -1)
Owned Dynamically Sized Types (Box<[T]>, Box<str>, Box<dyn Trait>)
Because dynamically sized types ([T], str, dyn Trait) do not have a compile-time known size, they cannot be owned directly as local variables. Box acts as a two-word fat pointer (16 bytes on 64-bit targets) that owns the heap-allocated DST:
| Boxed DST | Stack Size (64-bit) | How to Construct | Advantage Over Growable Counterpart |
|---|---|---|---|
Box<[T]> |
16 bytes (ptr, len) |
vec.into_boxed_slice() or Box::from(&slice[..]) |
Saves 8 bytes (cap word) compared to Vec<T> (24 bytes) and forbids accidental reallocation |
Box<str> |
16 bytes (ptr, len) |
string.into_boxed_str() or Box::from("text") |
Saves 8 bytes (cap word) compared to String (24 bytes) for immutable heap strings |
Box<dyn Trait> |
16 bytes (ptr, vtable) |
Box::new(concrete_value) coerced to Box<dyn Trait> |
Allows heterogeneous collections (Vec<Box<dyn Trait>>) with single ownership |
trait Job {
fn execute(&self) -> String;
}
struct EmailJob {
recipient: Box<str>,
}
impl Job for EmailJob {
fn execute(&self) -> String {
format!("Sent email to {}", self.recipient)
}
}
struct BackupJob {
shards: Box<[u16]>,
}
impl Job for BackupJob {
fn execute(&self) -> String {
format!("Backed up {} shards: {:?}", self.shards.len(), self.shards)
}
}
fn main() {
let queue: Vec<Box<dyn Job>> = vec![
Box::new(EmailJob {
recipient: String::from("ops@example.com").into_boxed_str(),
}),
Box::new(BackupJob {
shards: vec![1, 2, 3].into_boxed_slice(),
}),
];
for job in &queue {
println!("{}", job.execute());
}
}
Sent email to ops@example.com
Backed up 3 shards: [1, 2, 3]
Runtime-Static Configuration with Box::leak
When configuration data is loaded at runtime (from environment variables or a file) and needs to be shared as a 'static reference across threads for the entire lifetime of the process without reference-counting overhead, Box::leak converts Box<T> into &'static mut T (which coerces to &'static T).
#[derive(Debug)]
struct AppConfig {
host: String,
port: u16,
}
fn init_config() -> &'static AppConfig {
let cfg = Box::new(AppConfig {
host: String::from("127.0.0.1"),
port: 8080,
});
Box::leak(cfg)
}
fn main() {
let static_cfg: &'static AppConfig = init_config();
println!("listening on {}:{}", static_cfg.host, static_cfg.port);
}
listening on 127.0.0.1:8080
Zero-Sized Allocations in Box<T>
When T is a zero-sized type (size_of::<T>() == 0, such as Box::new(()) or Box::<[u8]>::from(&[][..])), Box does not call the global heap allocator. Instead, it stores a dangling, properly aligned non-null pointer (NonNull::dangling()) and performs a no-op on Drop.