Skip to content

Rust - Smart Pointers

In Rust, values are allocated on the stack by default, which is fast and efficient. However, sometimes we need to allocate data on the heap. This is necessary for data whose size is unknown at compile time, for transferring ownership of large amounts of data without expensive copying, or for creating recursive data structures. Smart pointers are data structures that act like pointers but also have additional metadata and capabilities, such as managing the memory they point to.

All smart pointers implement the Deref and Drop traits, which give them their special abilities beyond what a regular struct offers.

TraitPackage & Description
Derefstd::ops::Deref
Allows an instance of a smart pointer struct to be treated like a reference. This enables code like *my_box to access the inner data.
Dropstd::ops::Drop
Allows you to customize the code that is run when an instance of a smart pointer goes out of scope. This is fundamental to Rust’s RAII (Resource Acquisition Is Initialization) pattern, ensuring automatic memory deallocation and resource cleanup.

This chapter will explore Box<T>, the simplest smart pointer, and then briefly introduce other crucial pointers like Rc<T> and RefCell<T>.

The Box<T> smart pointer, often just called a “box”, is the most straightforward way to allocate a value on the heap. When you use a Box<T>, the pointer itself is stored on the stack, while the data it points to (T) resides on the heap. A Box<T> has no performance overhead other than the initial heap allocation.

  • Recursive Types: For types whose definition refers to themselves, like a linked list. The compiler needs to know the size of a type at compile time, and Box<T> breaks the infinite recursion by storing the recursive part on the heap (a pointer has a known, fixed size).
  • Large Data: To transfer ownership of a large amount of data without copying it. Moving a Box<T> on the stack is cheap, as only the pointer is moved.
  • Trait Objects: To use a value whose type is determined at runtime (dynamic dispatch), which we’ll cover in advanced topics.

Here’s how to use a Box to store an i32 value on the heap. The variable b is on the stack and holds the address of the value 5 on the heap.

fn main() {
// This value is on the stack
let stack_var: i32 = 5;
// This value is allocated on the heap
// 'b' is a Box<i32> on the stack, pointing to the heap data
let b = Box::new(stack_var);
println!("b = {}", b);
// To access the data inside the box, we must dereference it
assert_eq!(5, *b);
println!("The value inside the box is: {}", *b);
}
// When 'b' goes out of scope at the end of main,
// the Drop trait is called, deallocating the box's memory from the heap.
b = 5
The value inside the box is: 5

To understand Box<T> better, let’s create our own simple version called MyBox<T>. This will demonstrate how the Deref and Drop traits work.

The Deref trait allows us to customize the behavior of the dereference operator (*). By implementing Deref, we can write *y and have Rust translate it to *(y.deref()).

use std::ops::Deref;
// A generic tuple struct named MyBox
struct MyBox<T>(T);
impl<T> MyBox<T> {
// A constructor-like function to create new instances
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// Implementing the Deref trait for MyBox<T>
impl<T> Deref for MyBox<T> {
// Associated type that specifies what `*` will return a reference to
type Target = T;
fn deref(&self) -> &Self::Target {
// We return a reference to the data inside the tuple struct
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
// Behind the scenes, Rust is running: *(y.deref())
assert_eq!(5, *y);
println!("Successfully dereferenced MyBox!");
}

The Drop trait is crucial for RAII. The code in the drop method runs automatically when the value goes out of scope. You rarely need to implement Drop yourself because Rust handles memory deallocation for standard types. However, it’s essential for managing other resources like file handles or network connections.

struct CustomSmartPointer {
data: String,
}
// Implementing the Drop trait
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
// This code runs when an instance is dropped
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointers created.");
// At the end of main, 'd' is dropped first, then 'c' (in reverse order of creation).
}
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Other Smart Pointers for Different Scenarios

Section titled “Other Smart Pointers for Different Scenarios”

Box<T> is for single ownership. Rust’s standard library provides other smart pointers for more complex scenarios:

  • Rc<T> (Reference Counting): Use when a value needs to have multiple owners. Rc<T> keeps track of the number of references to a value. The data is deallocated from the heap only when the last reference goes out of scope. It’s for single-threaded scenarios.
  • RefCell<T> (Interior Mutability): Allows you to mutate data even when there are immutable references to it. This pattern enforces Rust’s borrowing rules at runtime instead of compile time. A violation of the rules (e.g., two mutable borrows at once) will cause a panic!.

Combining Rc<T> and RefCell<T> is a common pattern for creating graphs or other complex data structures where a value is owned by multiple others and may need to be mutated.