Rust - Smart Pointers
Rust - Smart Pointers
Section titled “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.
| Trait | Package & Description |
|---|---|
| Deref | std::ops::DerefAllows an instance of a smart pointer struct to be treated like a reference. This enables code like *my_box to access the inner data. |
| Drop | std::ops::DropAllows 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 Smart Pointer
Section titled “The Box Smart Pointer”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.
When to Use Box
Section titled “When to Use Box”- 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.
Example: Storing a Value on the Heap
Section titled “Example: Storing a Value on the Heap”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.Output
Section titled “Output”b = 5The value inside the box is: 5Implementing Your Own Smart Pointer
Section titled “Implementing Your Own Smart Pointer”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.
Implementing the Deref Trait
Section titled “Implementing the Deref Trait”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 MyBoxstruct 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!");}Implementing the Drop Trait
Section titled “Implementing the Drop Trait”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 traitimpl 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).}Output
Section titled “Output”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 apanic!.
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.