Lesson 56 of 94
Box: Heap Allocation
Values live wherever their owner is: on the stack, inside another struct, or inside a collection's buffer. A smart pointer is a type that owns a value stored somewhere else and acts like a reference to it. This section covers the four you'll use:
| Smart pointer | Owns |
|---|---|
Box<T> |
one value on the heap, single owner |
Rc<T> / Arc<T> |
one value, shared by several owners (reference counted) |
RefCell<T> / Cell<T> |
a value you can change through a shared reference |
Cow<T> |
either borrowed or owned data |
Box<T> is the simplest: it puts a value on the heap and owns it. When the Box is dropped, the value is freed. You don't need it often, but three situations require it: recursive types, trait objects, and very large values you want to move cheaply.
1. Putting a value on the heap
Box::new(value) moves the value to the heap. The Box itself is just a pointer on the stack.
fn main() {
let on_stack = 42;
let on_heap = Box::new(42);
println!("{on_stack} {on_heap}");
println!("sum: {}", on_stack + *on_heap);
println!("Box<i32> is {} bytes (a pointer)", std::mem::size_of::<Box<i32>>());
}42 42
sum: 84
Box<i32> is 8 bytes (a pointer)*on_heap dereferences the box to get the value, exactly like a reference. println! dereferences automatically.
2. A Box owns its value
Moving a Box moves the pointer, not the value. When the owner goes out of scope, the heap memory is freed.
fn make_config() -> Box<String> {
Box::new(String::from("mode=production"))
}
fn main() {
let config = make_config();
let moved = config; // moves the pointer; the String stays where it is
println!("{moved}");
// println!("{config}"); // error: value moved
} // `moved` is dropped: the String and the box's heap allocation are freedmode=production3. Methods work through a Box
A Box<T> derefs to T, so you call T's methods directly.
fn main() {
let names: Box<Vec<&str>> = Box::new(vec!["ana", "ben"]);
println!("{} names, first {:?}", names.len(), names.first());
let mut counter = Box::new(0);
*counter += 5;
println!("{counter}");
}2 names, first Some("ana")
54. You rarely need Box for ordinary values
It's tempting to box everything. Don't: a Vec, String, or HashMap already keeps its contents on the heap, and plain structs are cheap to move. Box<Vec<T>> just adds an extra pointer hop.
struct Order {
id: u32,
items: Vec<String>, // already heap-allocated inside; no Box needed
}
fn main() {
let o = Order { id: 1, items: vec![String::from("lamp")] };
println!("Order is {} bytes on the stack; its items live on the heap", std::mem::size_of::<Order>());
println!("{} {:?}", o.id, o.items);
}Order is 32 bytes on the stack; its items live on the heap
1 ["lamp"]5. Recursive types need a Box
A type that contains itself directly would have infinite size. A Box has a fixed size (one pointer), which breaks the recursion. You saw this with Expr in Enums.
enum List {
Cons(i32, List),
Nil,
}
fn main() {}error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +6. A singly linked list
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
impl List {
fn sum(&self) -> i32 {
match self {
Cons(value, rest) => value + rest.sum(),
Nil => 0,
}
}
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:?}", list);
println!("sum = {}", list.sum());
}Cons(1, Cons(2, Cons(3, Nil)))
sum = 6Linked lists are mostly a teaching example; a Vec or VecDeque is faster for almost every real workload.
7. A binary search tree
Each node owns its children through Option<Box<Node>>: None for no child. Option<Box<T>> is the same size as a plain pointer, thanks to the null-pointer optimization from Option.
#[derive(Debug)]
struct Node {
key: u32,
left: Option<Box<Node>>,
right: Option<Box<Node>>,
}
impl Node {
fn new(key: u32) -> Self {
Node { key, left: None, right: None }
}
fn insert(&mut self, key: u32) {
let child = if key < self.key { &mut self.left } else { &mut self.right };
match child {
Some(node) => node.insert(key),
None => *child = Some(Box::new(Node::new(key))),
}
}
fn contains(&self, key: u32) -> bool {
if key == self.key {
return true;
}
let child = if key < self.key { &self.left } else { &self.right };
child.as_ref().is_some_and(|n| n.contains(key))
}
fn in_order(&self, out: &mut Vec<u32>) {
if let Some(l) = &self.left {
l.in_order(out);
}
out.push(self.key);
if let Some(r) = &self.right {
r.in_order(out);
}
}
}
fn main() {
let mut root = Node::new(50);
for k in [30, 70, 20, 40, 60, 80] {
root.insert(k);
}
let mut sorted = Vec::new();
root.in_order(&mut sorted);
println!("{:?}", sorted);
println!("contains 60: {}, contains 65: {}", root.contains(60), root.contains(65));
}[20, 30, 40, 50, 60, 70, 80]
contains 60: true, contains 65: false8. Trait objects: Box<dyn Trait>
Box<dyn Trait> is the most common use of Box, covered in Trait Objects. The box gives every implementation the same size, so they fit in one collection.
trait Notifier {
fn send(&self, msg: &str) -> String;
}
struct Email(&'static str);
struct Sms(&'static str);
impl Notifier for Email {
fn send(&self, msg: &str) -> String {
format!("email to {}: {msg}", self.0)
}
}
impl Notifier for Sms {
fn send(&self, msg: &str) -> String {
format!("sms to {}: {msg}", self.0)
}
}
fn main() {
let channels: Vec<Box<dyn Notifier>> = vec![Box::new(Email("ops@x.com")), Box::new(Sms("+1555"))];
for c in &channels {
println!("{}", c.send("deploy finished"));
}
}email to ops@x.com: deploy finished
sms to +1555: deploy finished9. Boxing closures and errors
Closures and errors of different types are also stored as Box<dyn Fn...> and Box<dyn Error>, which you've already used.
use std::error::Error;
fn pick(op: &str) -> Box<dyn Fn(i64, i64) -> i64> {
match op {
"+" => Box::new(|a, b| a + b),
"*" => Box::new(|a, b| a * b),
_ => Box::new(|a, _| a),
}
}
fn parse(s: &str) -> Result<i64, Box<dyn Error>> {
Ok(s.parse()?)
}
fn main() {
println!("{} {}", pick("+")(2, 3), pick("*")(2, 3));
println!("{:?}", parse("x").map_err(|e| e.to_string()));
}5 6
Err("invalid digit found in string")10. Moving a large value cheaply
Moving a value copies its bytes. For a very large struct, boxing it means only an 8-byte pointer is copied on each move. It can also prevent stack overflows from huge local arrays.
struct Frame {
pixels: [u8; 1_000_000], // 1 MB, stored inline
}
fn render(frame: Box<Frame>) -> Box<Frame> {
frame // moving the Box copies 8 bytes, not 1 MB
}
fn main() {
let frame = Box::new(Frame { pixels: [7; 1_000_000] });
let frame = render(frame);
println!("{} bytes of pixels, first = {}", frame.pixels.len(), frame.pixels[0]);
println!("size of Frame: {} bytes, size of Box<Frame>: {} bytes", std::mem::size_of::<Frame>(), std::mem::size_of::<Box<Frame>>());
}1000000 bytes of pixels, first = 7
size of Frame: 1000000 bytes, size of Box<Frame>: 8 bytesIn practice a Vec<u8> is the more common choice for large buffers; Box<[u8; N]> is useful when the size is fixed.
11. Shrinking an enum by boxing a large variant
An enum is as big as its largest variant. Boxing a rarely used large variant makes every value of the enum smaller.
#[allow(dead_code)]
enum Message {
Ping,
Text(String),
Snapshot([u64; 64]), // 512 bytes
}
#[allow(dead_code)]
enum SmallMessage {
Ping,
Text(String),
Snapshot(Box<[u64; 64]>), // 8 bytes
}
fn main() {
println!("Message: {} bytes", std::mem::size_of::<Message>());
println!("SmallMessage: {} bytes", std::mem::size_of::<SmallMessage>());
}Message: 520 bytes
SmallMessage: 24 bytes#[allow(dead_code)] silences warnings about variants this example never constructs.
12. Box<[T]> and Box<str>: fixed-size owned buffers
A Vec<T> stores a pointer, length, and capacity (24 bytes). When a buffer will never grow again, into_boxed_slice() drops the capacity (16 bytes) and any spare memory. The same goes for String and Box<str>.
fn main() {
let mut v = Vec::with_capacity(100);
v.extend([1, 2, 3]);
let frozen: Box<[i32]> = v.into_boxed_slice();
println!("{:?} len {}", frozen, frozen.len());
let name: Box<str> = String::from("ferris").into_boxed_str();
println!("{name} ({} bytes)", name.len());
println!("Vec<i32> {} bytes, Box<[i32]> {} bytes", std::mem::size_of::<Vec<i32>>(), std::mem::size_of::<Box<[i32]>>());
}[1, 2, 3] len 3
ferris (6 bytes)
Vec<i32> 24 bytes, Box<[i32]> 16 bytes13. Deref coercion: &Box<T> works as &T
A function that takes &T accepts a &Box<T> automatically. This is deref coercion, explained fully in Drop, Deref & Cow.
fn shout(s: &str) -> String {
s.to_uppercase()
}
fn total(values: &[u32]) -> u32 {
values.iter().sum()
}
fn main() {
let boxed_text: Box<String> = Box::new(String::from("hello"));
let boxed_values: Box<Vec<u32>> = Box::new(vec![1, 2, 3]);
println!("{} {}", shout(&boxed_text), total(&boxed_values));
}HELLO 614. Moving a value out of a Box
Dereferencing a Box with * moves the value out and frees the box's allocation.
fn main() {
let boxed = Box::new(String::from("unboxed now"));
let plain: String = *boxed;
println!("{plain}");
}unboxed now15. Box implements the traits of what it holds
Box<T> is Debug, Clone, PartialEq, Ord, Display, and so on whenever T is.
fn main() {
let a = Box::new(vec![3, 1, 2]);
let mut b = a.clone();
b.sort();
println!("{:?} {:?} equal: {}", a, b, a == b);
println!("{}", Box::new(3.5) > Box::new(1.0));
}[3, 1, 2] [1, 2, 3] equal: false
true16. Realistic example: an expression evaluator
A small calculator whose syntax tree uses Box for its recursive nodes, with evaluation and pretty printing.
enum Expr {
Num(f64),
Var(&'static str),
Bin(Box<Expr>, char, Box<Expr>),
}
use Expr::*;
fn bin(l: Expr, op: char, r: Expr) -> Expr {
Bin(Box::new(l), op, Box::new(r))
}
impl Expr {
fn eval(&self, vars: &[(&str, f64)]) -> Result<f64, String> {
match self {
Num(n) => Ok(*n),
Var(name) => vars.iter().find(|(v, _)| v == name).map(|(_, x)| *x).ok_or(format!("unknown variable {name}")),
Bin(l, op, r) => {
let (a, b) = (l.eval(vars)?, r.eval(vars)?);
match op {
'+' => Ok(a + b),
'-' => Ok(a - b),
'*' => Ok(a * b),
'/' if b == 0.0 => Err(String::from("division by zero")),
'/' => Ok(a / b),
_ => Err(format!("unknown operator {op}")),
}
}
}
}
fn show(&self) -> String {
match self {
Num(n) => n.to_string(),
Var(v) => v.to_string(),
Bin(l, op, r) => format!("({} {op} {})", l.show(), r.show()),
}
}
}
fn main() {
// price * (1 + tax) - discount
let e = bin(bin(Var("price"), '*', bin(Num(1.0), '+', Var("tax"))), '-', Var("discount"));
println!("{}", e.show());
println!("{:?}", e.eval(&[("price", 100.0), ("tax", 0.2), ("discount", 15.0)]));
println!("{:?}", e.eval(&[("price", 100.0)]));
println!("{:?}", bin(Num(1.0), '/', Num(0.0)).eval(&[]));
}((price * (1 + tax)) - discount)
Ok(105.0)
Err("unknown variable tax")
Err("division by zero")17. Realistic example: a file-system tree
Directories own their children; Vec<Node> already provides the indirection, so no Box is needed here, which is worth recognizing.
enum Node {
File { name: String, bytes: u64 },
Dir { name: String, children: Vec<Node> },
}
impl Node {
fn size(&self) -> u64 {
match self {
Node::File { bytes, .. } => *bytes,
Node::Dir { children, .. } => children.iter().map(Node::size).sum(),
}
}
fn print(&self, depth: usize) {
match self {
Node::File { name, bytes } => println!("{}{name} ({bytes} B)", " ".repeat(depth)),
Node::Dir { name, children } => {
println!("{}{name}/ ({} B)", " ".repeat(depth), self.size());
for c in children {
c.print(depth + 1);
}
}
}
}
}
fn main() {
let tree = Node::Dir {
name: String::from("project"),
children: vec![
Node::File { name: String::from("Cargo.toml"), bytes: 220 },
Node::Dir {
name: String::from("src"),
children: vec![Node::File { name: String::from("main.rs"), bytes: 1_450 }, Node::File { name: String::from("lib.rs"), bytes: 3_100 }],
},
],
};
tree.print(0);
}project/ (4770 B)
Cargo.toml (220 B)
src/ (4550 B)
main.rs (1450 B)
lib.rs (3100 B)18. Realistic example: a plugin registry
Plugins are created from names at runtime and stored as Box<dyn Plugin>.
trait Plugin {
fn name(&self) -> &str;
fn transform(&self, input: &str) -> String;
}
struct Upper;
struct Reverse;
struct Censor(Vec<&'static str>);
impl Plugin for Upper {
fn name(&self) -> &str {
"upper"
}
fn transform(&self, input: &str) -> String {
input.to_uppercase()
}
}
impl Plugin for Reverse {
fn name(&self) -> &str {
"reverse"
}
fn transform(&self, input: &str) -> String {
input.chars().rev().collect()
}
}
impl Plugin for Censor {
fn name(&self) -> &str {
"censor"
}
fn transform(&self, input: &str) -> String {
self.0.iter().fold(input.to_string(), |acc, w| acc.replace(w, &"*".repeat(w.len())))
}
}
fn create(name: &str) -> Option<Box<dyn Plugin>> {
match name {
"upper" => Some(Box::new(Upper)),
"reverse" => Some(Box::new(Reverse)),
"censor" => Some(Box::new(Censor(vec!["darn", "heck"]))),
_ => None,
}
}
fn main() {
let configured = ["censor", "upper", "unknown"];
let pipeline: Vec<Box<dyn Plugin>> = configured.iter().filter_map(|n| create(n)).collect();
let names: Vec<&str> = pipeline.iter().map(|p| p.name()).collect();
let out = pipeline.iter().fold(String::from("well darn it"), |text, p| p.transform(&text));
println!("{:?}: {out}", names);
}["censor", "upper"]: WELL **** IT19. Realistic example: a state machine with boxed states
Each state is its own type implementing a State trait, and a transition returns the next state as Box<dyn State>. This is the classic GoF State pattern.
trait State {
fn name(&self) -> &'static str;
fn next(self: Box<Self>, event: &str) -> Box<dyn State>;
}
struct Draft;
struct Review {
approvals: u8,
}
struct Published;
impl State for Draft {
fn name(&self) -> &'static str {
"draft"
}
fn next(self: Box<Self>, event: &str) -> Box<dyn State> {
if event == "submit" { Box::new(Review { approvals: 0 }) } else { self }
}
}
impl State for Review {
fn name(&self) -> &'static str {
"review"
}
fn next(self: Box<Self>, event: &str) -> Box<dyn State> {
match event {
"approve" if self.approvals + 1 >= 2 => Box::new(Published),
"approve" => Box::new(Review { approvals: self.approvals + 1 }),
"reject" => Box::new(Draft),
_ => self,
}
}
}
impl State for Published {
fn name(&self) -> &'static str {
"published"
}
fn next(self: Box<Self>, _event: &str) -> Box<dyn State> {
self
}
}
fn main() {
let mut state: Box<dyn State> = Box::new(Draft);
for event in ["approve", "submit", "approve", "reject", "submit", "approve", "approve", "submit"] {
state = state.next(event);
println!("{event:<8} -> {}", state.name());
}
}approve -> draft
submit -> review
approve -> review
reject -> draft
submit -> review
approve -> review
approve -> published
submit -> publishedself: Box<Self> means the method consumes the boxed state, so a transition can reuse the same allocation (self) or return a new one.
20. Realistic example: a recursive JSON-like pretty printer
enum Value {
Num(f64),
Str(String),
List(Vec<Value>),
Pair(Box<Value>, Box<Value>),
}
fn render(v: &Value, indent: usize) -> String {
let pad = " ".repeat(indent);
match v {
Value::Num(n) => format!("{pad}{n}"),
Value::Str(s) => format!("{pad}\"{s}\""),
Value::List(items) => {
let inner: Vec<String> = items.iter().map(|i| render(i, indent + 1)).collect();
format!("{pad}[\n{}\n{pad}]", inner.join(",\n"))
}
Value::Pair(k, v) => format!("{pad}{} => {}", render(k, 0), render(v, 0)),
}
}
fn main() {
let v = Value::List(vec![
Value::Num(1.5),
Value::Str(String::from("rust")),
Value::Pair(Box::new(Value::Str(String::from("key"))), Box::new(Value::Num(42.0))),
Value::List(vec![Value::Num(1.0), Value::Num(2.0)]),
]);
println!("{}", render(&v, 0));
}[
1.5,
"rust",
"key" => 42,
[
1,
2
]
]21. Realistic example: building a list with a cursor
Walking a Box-linked list with a mutable cursor is a classic exercise in borrowing. The cursor is a &mut Option<Box<Node>> that always points at the next empty slot.
struct Node {
value: i32,
next: Option<Box<Node>>,
}
fn from_slice(values: &[i32]) -> Option<Box<Node>> {
let mut head: Option<Box<Node>> = None;
let mut cursor = &mut head;
for &v in values {
let node = cursor.insert(Box::new(Node { value: v, next: None }));
cursor = &mut node.next;
}
head
}
fn to_vec(mut node: &Option<Box<Node>>) -> Vec<i32> {
let mut out = Vec::new();
while let Some(n) = node {
out.push(n.value);
node = &n.next;
}
out
}
fn main() {
let list = from_slice(&[4, 8, 15, 16, 23, 42]);
println!("{:?}", to_vec(&list));
}[4, 8, 15, 16, 23, 42]Option::insert stores a value in the Option and returns a &mut to it, which is exactly what the cursor needs.
22. When to use Box
| Situation | Use Box? |
|---|---|
| A recursive enum or struct | Yes, for the recursive field (or use a Vec) |
| A collection of different types implementing a trait | Yes: Vec<Box<dyn Trait>> |
| Returning different closure or error types | Yes: Box<dyn Fn...>, Box<dyn Error> |
| A very large value moved around often | Sometimes |
An ordinary struct, String, or Vec |
No: they're already cheap to move |
fn main() {
let should_box = |recursive: bool, dyn_trait: bool, huge: bool| recursive || dyn_trait || huge;
println!("recursive tree: {}", should_box(true, false, false));
println!("plain config struct: {}", should_box(false, false, false));
}recursive tree: true
plain config struct: falseSummary
Box<T>owns a single value on the heap. It's a pointer-sized value that frees its contents when dropped.- Use it for recursive types, trait objects (
Box<dyn Trait>), boxed closures and errors, and large values you move often. Box<T>derefs toT: methods work directly,*bgets the value, and&Box<T>coerces to&T.Box<[T]>andBox<str>are compact owned buffers that never grow.- Don't box ordinary values; Rust values don't need to be on the heap.
Next: Rc & Weak, for values with more than one owner.
