In the previous tutorial, we learned closures and iterators. Now we learn smart pointers — types that act like pointers but have extra capabilities.
In Rust, the most common pointer is a reference (&T). References borrow data but do not own it. Smart pointers own the data they point to. They also add features like heap allocation, reference counting, and interior mutability.
What Is a Smart Pointer?
A smart pointer is a struct that:
- Implements the
Dereftrait — so it behaves like a reference - Implements the
Droptrait — so it cleans up when it goes out of scope
You already know two smart pointers: String (owns heap-allocated text) and Vec<T> (owns a heap-allocated array). In this tutorial, we learn four more: Box<T>, Rc<T>, Arc<T>, and RefCell<T>.
Box<T>: Heap Allocation
Box<T> is the simplest smart pointer. It puts data on the heap instead of the stack:
fn main() {
let boxed = Box::new(42);
println!("Value: {}", boxed); // 42
println!("Plus one: {}", *boxed + 1); // 43
}
The value 42 is stored on the heap. boxed is a pointer on the stack that points to it. When boxed goes out of scope, the heap memory is freed.
When to Use Box
1. Large data that you do not want to copy:
let big_array = Box::new([0u8; 1000]);
println!("Length: {}", big_array.len());
Without Box, this array lives on the stack. With Box, only an 8-byte pointer is on the stack.
2. Recursive types:
This does not compile:
enum List<T> {
Cons(T, List<T>), // ERROR: infinite size
Nil,
}
The compiler cannot calculate the size of List because it contains itself. Fix it with Box:
#[derive(Debug, PartialEq)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
Now Cons stores a value and a pointer (fixed size) to the next node:
impl<T> List<T> {
fn new() -> List<T> {
List::Nil
}
fn push(self, value: T) -> List<T> {
List::Cons(value, Box::new(self))
}
fn len(&self) -> usize {
match self {
List::Nil => 0,
List::Cons(_, rest) => 1 + rest.len(),
}
}
}
fn main() {
let list = List::new().push(3).push(2).push(1);
println!("Length: {}", list.len()); // 3
}
3. Trait objects:
fn create_greeter(formal: bool) -> Box<dyn Fn(&str) -> String> {
if formal {
Box::new(|name| format!("Good day, {}.", name))
} else {
Box::new(|name| format!("Hey, {}!", name))
}
}
The Deref Trait
The Deref trait lets you use * on your type, just like a reference. It also enables deref coercion — automatic conversion between reference types.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(value: T) -> MyBox<T> {
MyBox(value)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
Now MyBox works like a reference:
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let my_box = MyBox::new(String::from("Alex"));
greet(&my_box); // Deref coercion: &MyBox<String> → &String → &str
}
Deref coercion happens automatically when types do not match. The compiler calls deref() as many times as needed to make the types line up.
The Drop Trait
The Drop trait lets you run code when a value goes out of scope. It is like a destructor in C++ or a finalizer in Java:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropped: {}", self.name);
}
}
fn main() {
let _r1 = Resource { name: String::from("file.txt") };
let _r2 = Resource { name: String::from("db_conn") };
println!("Resources created");
}
// Output:
// Resources created
// Dropped: db_conn ← r2 dropped first (reverse order)
// Dropped: file.txt ← r1 dropped second
Values are dropped in reverse order of creation. This is important for cleanup — if r2 depends on r1, r2 is cleaned up first.
Early Drop
You can drop a value early with std::mem::drop():
let resource = Resource { name: String::from("temp") };
drop(resource); // Dropped now, not at end of scope
println!("After drop");
You cannot call resource.drop() directly — Rust prevents double-free by requiring you to use std::mem::drop().
Rc<T>: Reference Counting
Sometimes you need multiple owners for the same data. Ownership rules say one owner, but Rc<T> lets you share:
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("shared data"));
let clone1 = Rc::clone(&data);
let clone2 = Rc::clone(&data);
println!("Count: {}", Rc::strong_count(&data)); // 3
println!("data: {}", data);
println!("clone1: {}", clone1);
println!("clone2: {}", clone2);
drop(clone1);
println!("After drop: {}", Rc::strong_count(&data)); // 2
}
Rc::clone() does not copy the data. It only increments the reference count. The data is freed when the last Rc is dropped and the count reaches zero.
Rc for Shared Tree Nodes
Rc is perfect for tree structures where nodes can have multiple parents:
use std::rc::Rc;
struct TreeNode {
value: i32,
children: Vec<Rc<TreeNode>>,
}
impl TreeNode {
fn new(value: i32) -> TreeNode {
TreeNode { value, children: Vec::new() }
}
fn with_children(value: i32, children: Vec<Rc<TreeNode>>) -> TreeNode {
TreeNode { value, children }
}
fn sum(&self) -> i32 {
let children_sum: i32 = self.children.iter().map(|c| c.sum()).sum();
self.value + children_sum
}
}
fn main() {
// Shared leaf — used by two branches
let leaf = Rc::new(TreeNode::new(10));
let branch1 = Rc::new(TreeNode::with_children(5, vec![Rc::clone(&leaf)]));
let branch2 = Rc::new(TreeNode::with_children(3, vec![Rc::clone(&leaf)]));
let root = TreeNode::with_children(1, vec![branch1, branch2]);
println!("Sum: {}", root.sum()); // 1 + 5 + 10 + 3 + 10 = 29
println!("Leaf count: {}", Rc::strong_count(&leaf)); // 3
}
Important: Rc<T> is not thread-safe. It only works in single-threaded code. For multi-threaded code, use Arc<T>.
RefCell<T>: Interior Mutability
Normally, you cannot mutate data behind a shared reference (&T). RefCell<T> lets you break this rule at runtime:
use std::cell::RefCell;
struct Counter {
count: RefCell<u32>,
name: String,
}
impl Counter {
fn new(name: &str) -> Counter {
Counter {
count: RefCell::new(0),
name: name.to_string(),
}
}
fn increment(&self) { // Takes &self, not &mut self
*self.count.borrow_mut() += 1;
}
fn get(&self) -> u32 {
*self.count.borrow()
}
}
fn main() {
let counter = Counter::new("clicks");
counter.increment(); // No &mut needed!
counter.increment();
counter.increment();
println!("{}: {}", counter.name, counter.get()); // clicks: 3
}
RefCell moves the borrowing rules from compile time to runtime:
borrow()— returns a shared reference (like&T)borrow_mut()— returns a mutable reference (like&mut T)
If you violate the rules (two mutable borrows at the same time), the program panics at runtime instead of failing at compile time.
Rc<RefCell<T>>: Shared Mutable Data
The most common pattern is combining Rc and RefCell:
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let data = Rc::new(RefCell::new(vec![String::from("initial")]));
let handle1 = Rc::clone(&data);
let handle2 = Rc::clone(&data);
handle1.borrow_mut().push(String::from("from handle1"));
handle2.borrow_mut().push(String::from("from handle2"));
println!("{:?}", data.borrow());
// ["initial", "from handle1", "from handle2"]
}
Both handle1 and handle2 can read and write the shared Vec. This is safe because RefCell checks the borrowing rules at runtime.
Arc<T>: Thread-Safe Reference Counting
Arc<T> is Rc<T> for multi-threaded code. “Arc” stands for Atomic Reference Counting. It uses atomic operations to safely update the count from multiple threads:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let sum: i32 = data_clone.iter().sum();
println!("Sum from thread: {}", sum);
});
println!("Original: {:?}", data);
handle.join().unwrap();
}
Arc is slightly slower than Rc because atomic operations have overhead. Use Rc when you do not need threads. Use Arc when you do.
Arc<Mutex<T>>: Thread-Safe Shared Mutable Data
For mutable shared data across threads, combine Arc with Mutex (which we cover in the next tutorial):
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Count: {}", *counter.lock().unwrap()); // 10
}
When to Use Which
| Type | Ownership | Thread-Safe | Mutable | Use Case |
|---|---|---|---|---|
Box<T> | Single owner | Yes | If owned mutably | Heap allocation, recursive types |
Rc<T> | Multiple owners | No | No (use RefCell) | Shared data, single thread |
Arc<T> | Multiple owners | Yes | No (use Mutex) | Shared data, multiple threads |
RefCell<T> | Single owner | No | Yes (runtime checked) | Interior mutability |
Rc<RefCell<T>> | Multiple owners | No | Yes | Shared mutable data, single thread |
Arc<Mutex<T>> | Multiple owners | Yes | Yes | Shared mutable data, multiple threads |
Start simple. Use owned values and references first. Reach for smart pointers only when you need them.
Common Mistakes
Mistake: Using Rc in Multi-Threaded Code
use std::rc::Rc;
use std::thread;
let data = Rc::new(42);
thread::spawn(move || {
println!("{}", data); // ERROR: Rc<i32> is not Send
});
Fix: Use Arc for threads:
use std::sync::Arc;
let data = Arc::new(42);
Mistake: Two Mutable Borrows on RefCell
let cell = RefCell::new(42);
let borrow1 = cell.borrow_mut();
let borrow2 = cell.borrow_mut(); // PANIC at runtime!
RefCell checks borrowing rules at runtime. Two mutable borrows at the same time cause a panic. Fix: Drop the first borrow before creating the second:
let cell = RefCell::new(42);
{
let mut borrow1 = cell.borrow_mut();
*borrow1 += 1;
} // borrow1 dropped here
{
let mut borrow2 = cell.borrow_mut();
*borrow2 += 1;
}
Mistake: Reference Cycles with Rc
Rc uses reference counting. If two Rc values point to each other, the count never reaches zero and the memory leaks:
// A -> B -> A -> B -> ... (cycle — memory leak!)
Fix: Use Weak<T> (a weak reference that does not increment the count) for back-references. This is important for tree structures where children point back to parents.
Decision Flowchart
Here is a simple guide:
Start with: owned values (String, Vec<T>, plain structs)
Need a reference? Use &T or &mut T
Need heap allocation? Use Box<T>
Need multiple owners, single thread? Use Rc<T>
Need multiple owners, multi thread? Use Arc<T>
Need to mutate shared data, single thread? Use Rc<RefCell<T>>
Need to mutate shared data, multi thread? Use Arc<Mutex<T>>
Most Rust programs only use Box and occasionally Arc<Mutex<T>>. Do not reach for Rc or RefCell unless you have a specific reason. Simple ownership is almost always better.
- Need heap allocation or recursive types? Use
Box<T>. - Need multiple owners? Use
Rc<T>(single thread) orArc<T>(multi thread). - Need to mutate through shared references? Add
RefCell<T>orMutex<T>.
Summary
| Concept | What It Does |
|---|---|
Box<T> | Heap-allocates a value, single owner |
Deref trait | Makes a type behave like a reference |
Drop trait | Runs cleanup when a value is dropped |
Rc<T> | Reference-counted shared ownership (single thread) |
Arc<T> | Atomic reference-counted shared ownership (multi thread) |
RefCell<T> | Interior mutability with runtime borrow checking |
Rc<RefCell<T>> | Shared mutable data (single thread) |
Source Code
Related Tutorials
- Rust Tutorial #12: Closures and Iterators — previous tutorial
- Rust Tutorial #14: Concurrency — next tutorial
What’s Next?
We now understand smart pointers and when to use each one. Next, we learn concurrency — threads, channels, and mutexes. We will see how Arc<Mutex<T>> works in practice, and why Rust’s ownership system makes concurrent programming safe.