Today, I began exploring smart pointers, one of the more advanced but essential topics in Rust. While pointers are a common concept across languages, smart pointers in Rust combine ownership, borrowing, and memory safety into powerful tools.
🔹 What Are Smart Pointers?
A pointer is simply a variable that contains the memory address of some data. The most familiar pointer in Rust is a reference (&
), which borrows data but doesn’t own it.
Smart pointers go beyond this. They are data structures that act like pointers but come with additional metadata and capabilities. Unlike references, smart pointers often own the data they point to.
Some examples of smart pointers in Rust:
-
String
andVec<T>
(yes, you’ve already been using them!) -
Box<T>
– Heap allocation -
Rc<T>
– Reference counting (multiple owners) -
RefCell<T>
– Borrowing rules enforced at runtime
Most smart pointers are implemented using structs and rely on two critical traits:
-
Deref
– Makes smart pointers behave like references. -
Drop
– Customizes behavior when the value goes out of scope.
In this post, I’ll focus on Box<T>
, the simplest smart pointer.
📦 Using Box to Point to Data on the Heap
A box lets you store data on the heap instead of the stack. On the stack, only a small pointer to the heap data is stored.
When to Use Box:
- When the size of a type is unknown at compile time.
- When transferring ownership of large data without copying it.
- When working with trait objects (polymorphism).
Example: Storing Data on the Heap
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
➡️ This stores 5
on the heap, and the box itself is on the stack.
When the box goes out of scope, both the stack pointer and heap memory are cleaned up automatically.
🔁 Enabling Recursive Types with Boxes
One of the most common uses of Box<T>
is to enable recursive types.
Let’s look at the classic cons list (a recursive data structure from Lisp).
First Attempt (Does Not Compile)
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
➡️ This fails because Rust cannot determine how much memory the recursive type needs—it could be infinite!
Fixing with Box
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
➡️ By wrapping the recursive type in Box
, Rust knows the size (a fixed pointer size) and can compile the program.
Why This Works
- Each
Cons
contains ani32
and a pointer (Box) to the next element. - The recursion ends with
Nil
. - Memory usage is predictable because Rust only needs to store the pointer size for the box.
🧠 Key Takeaways
- Smart pointers own the data they point to, unlike references.
-
Box<T>
moves data to the heap but keeps pointer size fixed on the stack. - Recursive types like linked lists require
Box<T>
to work. - Boxes are lightweight: they provide indirection without extra overhead.
- Traits
Deref
andDrop
make smart pointers behave seamlessly.
This was my first step into smart pointers. Next, I’ll explore Deref, Drop, and other smart pointers like Rc<T>
and RefCell<T>
, along with advanced patterns like interior mutability and reference cycles.
🚀 Follow me for more updates on my #100DaysOfRust journey!
Top comments (0)