Smart pointers are data structures that act like pointers but also have additional metadata and capabilities. In Rust, they are a key feature of the ownership system, allowing for memory management and mutation rules that are enforced at compile time. Unlike ordinary references, which only borrow data, smart pointers often own the data they point to.
Common smart pointers in Rust’s standard library include:
Box<T>for allocating values on the heap.Rc<T>for multiple-owner reference-counted pointers.Ref<T>andRefMut<T>, accessed throughRefCell<T>, for enforcing borrowing rules at runtime instead of compile time.
The “smart” behavior of these pointers comes from two crucial traits: Deref and Drop.
The Deref Trait: Accessing the Inner Value
The Deref trait allows a smart pointer struct to behave like a reference. Implementing Deref on a type lets you customize the behavior of the dereference operator (*). When you use * on a smart pointer, Rust calls the deref method and then performs a standard dereference.
This is what allows you to write code that works seamlessly with both smart pointers and regular references. A powerful related feature is deref coercion, where a smart pointer can be automatically converted into a reference to the type it contains.
Let’s create a simple smart pointer, MyBox<T>, to see Deref in action.
use std::ops::Deref;
// A simple smart pointer that owns heap-allocated data.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// Implementing the Deref trait for MyBox.
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
// We can use the dereference operator `*` on `y`
// because we implemented the Deref trait.
assert_eq!(5, *y);
let m = MyBox::new(String::from("Rust"));
// Deref coercion in action:
// `&m` (a &MyBox<String>) is coerced into a `&String`,
// which is then coerced into a `&str`.
hello(&m);
}
// This function takes a string slice.
fn hello(name: &str) {
println!("Hello, {}!", name);
}
Without the Deref trait, the compiler would only know how to work with MyBox<T> itself, not the value T inside it.
The Drop Trait: Cleaning Up
The Drop trait is Rust’s version of a destructor. It allows you to run custom code when a value goes out of scope. This is essential for smart pointers that manage resources like heap memory, file handles, or network connections. When the smart pointer is dropped, its implementation of Drop can deallocate the memory or release the resource.
Rust automatically calls the drop method for any type that implements the Drop trait. You are not allowed to call it manually.
Here’s an example of a custom smart pointer with a Drop implementation to show when it’s called.
struct CustomSmartPointer {
data: String,
}
// Implementing the Drop trait.
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
// This code runs when the instance goes out of scope.
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.");
// `d` is dropped here, at the end of the inner scope.
// `c` is dropped here, at the end of the main scope.
}
When you run this code, you’ll see the “Dropping…” messages printed as each CustomSmartPointer goes out of scope, demonstrating Rust’s deterministic resource management. If you need to force a value to be cleaned up early, you can use the std::mem::drop function.
Conclusion
The Deref and Drop traits are the foundation of Rust’s smart pointers. Deref provides ergonomic access to the data a pointer contains, while Drop ensures that resources are cleaned up automatically and safely. Together, they enable the creation of powerful, safe, and efficient abstractions for resource management in Rust.