Rust - Boxing and Unboxing
References
- https://doc.rust-lang.org/rust-by-example/std/box.html
- https://doc.rust-lang.org/std/boxed/struct.Box.html
- https://doc.rust-lang.org/std/boxed/index.html
- https://www.linkedin.com/pulse/boxing-unboxing-rust-luis-soares-m-sc-
All values in Rust are stack allocated by default. Values can be boxed (allocated on the heap) by creating a Box<T>. A box is a smart pointer to a heap allocated value of type T. When a box goes out of scope, its destructor is called, the inner object is destroyed, and the memory on the heap is freed.
Box<T>, casually referred to as a ‘box’, provides the simplest form of heap allocation in Rust. Boxes provide ownership for this allocation, and drop their contents when they go out of scope. Boxes also ensure that they never allocate more than isize::MAX bytes.
Boxing
in Rust refers to the process of allocating data on the heap and storing a reference to it on the stack. This is achieved using the Box type. When you box a value, you essentially wrap it inside aBox
and thus move it to the heap.Unboxing
, conversely, is the process of dereferencing a boxed value to access the data it contains. In Rust, you can use the*
operator to dereference a boxed value. After unboxing, if there are no remaining references to the boxed value, the memory for it will be deallocated.

Why use Boxing?
There are several reasons why you’d want to use boxing in Rust:
Dynamic Size
: Some data structures, like linked lists, require efficient or feasible indirection. For data with a size unknown at compile time, or for recursive data structures where an instance can contain another instance of the same type, you’ll need to use boxes.Trait Objects
: When working with trait objects, you’d often use a Box to store instances of types that implement a particular trait. This way, you can uniformly work with different types.Transfer of Ownership
: Sometimes you’d want to transfer ownership of a value without copying the data. Boxing helps with this, especially in scenarios where you wish to ensure the data remains allocated for the program’s lifetime, even if the original owner goes out of scope.Concurrency and Shared State
: For shared state across threads, you’d use Arc, a thread-safe reference-counted box.
When to Use Boxing?
When Stack Allocation is Unsuitable
: The stack is fast but limited in size. If a value is too large or its size is unknown at compile time, it’s a candidate for heap allocation, and thus boxing.For Recursive Data Types
: Consider the classic example of a linked list. Each node might contain the next node of the same type. Such a recursive structure is not possible without boxing in Rust.enum List<T> { Cons(T, Box<List<T>>), Nil, }
Trait Objects
: If you want to store multiple types that implement a given trait in a homogeneous collection, you’d use a box.let my_shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle {...}), Box::new(Rectangle {...})];
Returning Dynamic Types from Functions
: A function might need to return different types based on its inputs in some scenarios. Boxing can be a solution here, coupled with trait objects.
Advanced Boxing Techniques
Rust offers advanced tools that build upon the concept of boxes:
Reference-Counted Boxes: Rc and Arc
Reference-counted boxes allow multiple ownership of data. When the last reference is dropped, the data is deallocated.
Rc (Single-threaded)
use std::rc::Rc;
let foo = Rc::new(vec![1.0, 2.0, 3.0]);
let a = foo.clone();
let b = foo.clone();
println!("Reference count after creating a: {}", Rc::strong_count(&foo));
println!("Reference count after creating b: {}", Rc::strong_count(&foo));
// When a and b go out of scope, the memory for the vector will be deallocated.
Arc (Multi-threaded)
use std::sync::Arc;
use std::thread;
let foo = Arc::new(vec![1.0, 2.0, 3.0]);
let a = foo.clone();
let b = foo.clone();
thread::spawn(move || {
println!("{:?}", a);
}).join().unwrap();
println!("{:?}", b);
// Memory will be deallocated after both threads finish.
Cell and RefCell
Both Cell and RefCell allow for interior mutability
, a way to mutate the data even when there’s an immutable reference to it.
Cell
Cell provides a way to change the inner value but only works for Copy types.
use std::cell::Cell;
let x = Cell::new(1);
let y = &x;
y.set(2);
println!("x: {}", x.get()); // Outputs: 2
RefCell
RefCell is more flexible than Cell and allows mutable borrows, but at runtime.
use std::cell::RefCell;
let x = RefCell::new(vec![1, 2, 3]);
{
let mut y = x.borrow_mut();
y.push(4);
}
println!("x: {:?}", x.borrow()); // Outputs: [1, 2, 3, 4]
Note: Borrowing a RefCell mutably while it’s already borrowed will panic at runtime.
Weak References
Weak references are used in conjunction with Rc or Arc and don’t increase the reference count. This can be helpful to break circular references.
use std::rc::{Rc, Weak};
struct Node {
value: i32,
next: Option<Rc<Node>>,
prev: Weak<Node>,
}
let node1 = Rc::new(Node {
value: 1,
next: None,
prev: Weak::new(),
});
let node2 = Rc::new(Node {
value: 2,
next: Some(node1.clone()),
prev: Rc::downgrade(&node1),
});
// You can upgrade a weak reference to an Rc using the upgrade() method.
let strong_reference = node2.prev.upgrade().unwrap();
println!("Node value: {}", strong_reference.value); // Outputs: 1
In this example, node2 has a weak reference (prev) to node1. Even though node1 is referenced by node2, the use of a weak reference ensures that it doesn’t affect the reference count of node1.
Potential Pitfalls and Best Practices
While boxing and unboxing are essential tools in Rust, they come with potential pitfalls and nuances that developers should be aware of.
Performance Overhead
: Heap allocation and deallocation in any language have overheads compared to stack allocation. Over-reliance on Box can lead to performance bottlenecks, especially in scenarios where high-speed operations are crucial. Before resorting to boxing, always consider if stack allocation or borrowing can achieve the desired result.Deep Recursive Structures
: Each node’s allocation can cause a performance hit for deeply recursive structures like trees. This can add up quickly for large trees.Memory Leaks
: While Rust’s ownership system ensures safety against many types of bugs, it’s still possible to create memory leaks, especially when using reference-counted boxes like Rc or Arc. Circular references can prevent values from being deallocated, leading to memory leaks. Always be careful with reference counts, ensuring that cycles are avoided or broken.Multiple Dereferencing
: Continuous dereferencing (e.g., **boxed_boxed_integer) can make code harder to read. It’s good to keep the dereference chain short or use intermediate variables with descriptive names to enhance code readability.