Smart Pointers in Rust
Smart pointers are data structures that act like pointers but have additional capabilities. Rust has several built-in smart pointers that help manage memory and ownership in different scenarios.
The most common ones are Box<T>, Rc<T>, and RefCell<T>. Each solves a specific problem that regular references can't handle.
fn main() {
// Box stores data on the heap
let boxed = Box::new(42);
println!("Boxed value: {}", boxed);
// Regular stack-allocated
let regular = 42;
println!("Regular value: {}", regular);
}
Try it Yourself ->
Box<T> for Heap Allocation
Box<T> allocates data on the heap instead of the stack. It's useful for large data, recursive types, or when you need owned data.
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
println!("{:?}", list);
let large_array = Box::new([0; 1000]);
println!("Array length: {}", large_array.len());
}
Try it Yourself ->
Rc<T> for Reference Counting
Rc<T> (Reference Counted) lets multiple owners share the same data. It's great for when you need shared ownership but not mutation.
use std::rc::Rc;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let data2 = Rc::clone(&data);
let data3 = Rc::clone(&data);
println!("Data: {:?}", data);
println!("Data2: {:?}", data2);
println!("Data3: {:?}", data3);
println!("Reference count: {}", Rc::strong_count(&data));
}
Try it Yourself ->
RefCell<T> for Interior Mutability
RefCell<T> allows you to mutate data even when there are immutable references to it. This is called interior mutability and is checked at runtime.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Borrow immutably
println!("Data: {:?}", data.borrow());
// Borrow mutably
data.borrow_mut().push(4);
println!("After push: {:?}", data.borrow());
}
Try it Yourself ->
The Deref Trait
Smart pointers implement the Deref trait, which lets them be used like references. This means you can call methods on them just like regular references.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = MyBox::new(5);
println!("Value: {}", *x);
let name = MyBox::new(String::from("Hello"));
hello(&name);
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
Try it Yourself ->
The Drop Trait
The Drop trait lets you run code when a value goes out of scope. This is how smart pointers clean up resources automatically.
struct CustomPointer {
data: String,
}
impl Drop for CustomPointer {
fn drop(&mut self) {
println!("Dropping CustomPointer with data: {}", self.data);
}
}
fn main() {
let a = CustomPointer { data: "first".to_string() };
let b = CustomPointer { data: "second".to_string() };
println!("CustomPointers created");
// Drop happens in reverse order
drop(a);
println!("a dropped explicitly");
}
Try it Yourself ->
When to Use Each Type
Use Box<T> for heap allocation and recursive types. Use Rc<T> for shared ownership without mutation. Use RefCell<T> for interior mutability. Combine Rc<RefCell<T>> for shared, mutable data.
These smart pointers are essential for building complex data structures and managing memory in Rust programs.