Smart Pointers
Smart pointers are data structures that act like pointers but have additional metadata and capabilities. Unlike regular references, smart pointers own the data they point to. Rust's smart pointers enable advanced patterns like heap allocation, reference counting, and interior mutability.
What Are Smart Pointers?
A pointer is a variable that contains an address in memory. In Rust, the most
common pointer is a reference (&). Smart pointers are data
structures that not only act like pointers but also have additional metadata and
capabilities.
Deref and Drop traits.
Common Smart Pointer Types
| Type | Purpose | Use Case |
|---|---|---|
Box<T> |
Heap allocation | Recursive types, large data, dynamic dispatch |
Rc<T> |
Reference counting | Multiple ownership (single-threaded) |
RefCell<T> |
Interior mutability | Mutate data with immutable references |
Arc<T> |
Atomic reference counting | Multiple ownership (multi-threaded) |
Box<T> - Heap Allocation
Box<T> is the most straightforward smart pointer. It allows
you to store data on the heap rather than the stack.
Click Run to execute your code
When to Use Box<T>
- Recursive types: Types whose size can't be known at compile time
- Large data: Transfer ownership without copying large amounts of data
- Trait objects: When you want a value with a type implementing a specific trait
Rc<T> - Reference Counting
Rc<T> enables multiple ownership by keeping track of the
number of references to a value. The value is dropped when the last reference
goes out of scope.
Click Run to execute your code
Rc<T> is only for single-threaded
scenarios. Use Arc<T> (Atomic Rc) for multi-threaded
programs.
Rc Methods
| Method | Description |
|---|---|
Rc::new(value) |
Create new Rc pointer |
Rc::clone(&rc) |
Create new reference (shallow copy) |
Rc::strong_count(&rc) |
Get number of strong references |
Rc::weak_count(&rc) |
Get number of weak references |
RefCell<T> - Interior Mutability
RefCell<T> provides interior mutability - a design pattern
that allows you to mutate data even when there are immutable references to that
data.
Click Run to execute your code
RefCell<T>,
borrowing rules are enforced at runtime instead of compile time. If you violate
the rules, your program will panic.
RefCell Methods
| Method | Returns | Description |
|---|---|---|
borrow() |
Ref<T> |
Immutable borrow (panics if mutably borrowed) |
borrow_mut() |
RefMut<T> |
Mutable borrow (panics if already borrowed) |
try_borrow() |
Result<Ref<T>> |
Safe immutable borrow |
try_borrow_mut() |
Result<RefMut<T>> |
Safe mutable borrow |
Combining Rc<T> and RefCell<T>
A common Rust pattern is combining Rc<T> and
RefCell<T> to have multiple owners with the ability to mutate
data.
Click Run to execute your code
Rc<RefCell<T>> is useful for
graph-like data structures where nodes need multiple owners and mutability.
The Deref Trait
The Deref trait allows you to customize the behavior of the
dereference operator *. Smart pointers implement Deref
so they can be treated like regular references.
Click Run to execute your code
Deref Coercion
Deref coercion automatically converts a reference to a type implementing
Deref into a reference to another type. This happens automatically
when you pass a reference as an argument to a function.
&String can be coerced to
&str because String implements
Deref<Target=str>.
The Drop Trait
The Drop trait lets you customize what happens when a value goes out
of scope. Smart pointers use Drop to clean up resources.
Click Run to execute your code
drop() method directly.
Use std::mem::drop() to drop a value early.
Best Practices
- Prefer ownership: Use smart pointers only when you need their specific capabilities
- Use Box for simple heap allocation: When you just need data on the heap
- Use Rc for shared ownership: When multiple parts of code need to read the same data
- Use RefCell sparingly: Runtime borrowing checks have overhead and can panic
- Avoid cycles:
Rccreates cycles that leak memory. UseWeakreferences - Document interior mutability: Make it clear when using
RefCellin your APIs
Common Mistakes
1. Creating reference cycles with Rc
Wrong:
// This creates a memory leak!
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
}
let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));
a.borrow_mut().next = Some(Rc::clone(&b)); // Cycle!
Fix - Use Weak references:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
prev: Option<Weak<RefCell<Node>>>, // Use Weak
}
2. Multiple mutable borrows with RefCell
Wrong:
let value = RefCell::new(5);
let borrow1 = value.borrow_mut();
let borrow2 = value.borrow_mut(); // Panic at runtime!
Fix - Drop borrows when done:
let value = RefCell::new(5);
{
let mut borrow1 = value.borrow_mut();
*borrow1 = 10;
} // borrow1 dropped here
let mut borrow2 = value.borrow_mut(); // OK!
3. Using Rc in multi-threaded code
Wrong:
use std::rc::Rc;
use std::thread;
let data = Rc::new(5);
thread::spawn(move || { // Error: Rc is not Send
println!("{}", data);
});
Fix - Use Arc instead:
use std::sync::Arc;
use std::thread;
let data = Arc::new(5);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("{}", data_clone);
});
Exercise: Smart Pointers Practice
Task: Implement a simple graph structure using Rc and RefCell to handle shared ownership and mutability.
Requirements:
- Create nodes that can have multiple neighbors
- Use
Rc<RefCell<Node>>for shared mutable ownership - Implement the ability to add neighbors to nodes
- Create a graph with cycles (node A points to B, B to C, C back to A)
Click Run to execute your code
Show Solution
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
neighbors: RefCell<Vec<Rc<RefCell<Node>>>>,
}
impl Node {
fn new(value: i32) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node {
value,
neighbors: RefCell::new(vec![]),
}))
}
fn add_neighbor(&mut self, neighbor: Rc<RefCell<Node>>) {
self.neighbors.borrow_mut().push(neighbor);
}
}
fn main() {
let node1 = Node::new(1);
let node2 = Node::new(2);
let node3 = Node::new(3);
// Connect the nodes in a cycle
node1.borrow_mut().add_neighbor(Rc::clone(&node2));
node2.borrow_mut().add_neighbor(Rc::clone(&node3));
node3.borrow_mut().add_neighbor(Rc::clone(&node1));
println!("Graph created with cycles!");
println!("Node 1 has {} neighbors", node1.borrow().neighbors.borrow().len());
}
Summary
- Smart pointers own the data they point to, unlike references
- Box<T> stores data on the heap with single ownership
- Rc<T> enables multiple ownership via reference counting
- RefCell<T> allows interior mutability with runtime borrow checks
- Rc<RefCell<T>> combines shared ownership with mutability
- Deref trait allows smart pointers to act like references
- Drop trait customizes cleanup when values go out of scope
- Arc<T> is the thread-safe version of Rc
- Weak<T> prevents reference cycles
- Smart pointers enable advanced patterns while maintaining memory safety
What's Next?
Now that you understand smart pointers, you're ready to learn about Packages, Crates, and Modules - Rust's module system for organizing code into reusable components and managing project structure.
Enjoying these tutorials?