Ah, Rust. The language that forces you to deal with memory like an overprotective parent hovering over a toddler near a swimming pool.
You can't just throw pointers around like it's C. No, no. Rust wants you to think about your memory.
But don't worry, you don't have to memorize The Book (although you probably should). This blog post is your (mostly funny, definitely useful) guide to all the Smart Pointers in Rust: what they do, when to use them, when NOT to use them, and how to make your code so safe even your mom could run it without crashing your kernel.
Also, if you're vibing with the low-level magic and want to prove your mettle with projects like this, check out my Codecrafters profile here.
🧠 What Even Is a Smart Pointer?
A smart pointer is like a regular pointer... but with a college degree. It knows how to do more than just point — it owns stuff, manages memory, maybe even handles cleanup, and occasionally it pays taxes (ok not really).
Rust's standard library gives us a few smarties:
Box<T>
Rc<T>
Arc<T>
RefCell<T>
Mutex<T>
RwLock<T>
Oh and we'll also talk about combos like Rc<RefCell<T>>
and Arc<Mutex<T>>
that make your code both powerful and an emotional rollercoaster. Let’s get to it!
📦 Box<T>
— The Minimalist Smartboi
What it is:
A heap-allocated pointer. You use it when you want to store data on the heap instead of the stack.
Why use it:
- You want recursive types (like linked lists)
- You want to heap-allocate something big
- You just want to look cool
Example:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
When to use:
- Recursive enums (Rust needs to know the size)
- You want indirection without ownership gymnastics
When not to use:
- When you need shared or mutable access
- When you need thread-safety
👯 Rc<T>
— The Cloneable Roommate (Single-threaded)
What it is:
Reference-counted pointer. For single-threaded shared ownership.
Why use it:
- You want multiple parts of your code to share ownership
- You don’t need thread safety (this thing will panic if you try to thread it)
Example:
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("shared"));
let b = a.clone();
let c = a.clone();
println!("{} {} {}", a, b, c);
}
When to use:
- Trees, graphs, or other shared data structures
- You're working in a single-threaded context
When not to use:
- In multi-threaded apps (this will end in tears)
🌐 Arc<T>
— The Thread-Safe Cloneable Roommate
What it is:
Atomic Reference Counted pointer. Same as Rc<T>
but works across threads.
Why use it:
- You want shared ownership across threads
Example:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
for _ in 0..3 {
let data = Arc::clone(&data);
thread::spawn(move || {
println!("{:?}", data);
});
}
}
When to use:
- Multithreaded shared ownership
- Clone-a-lot situations in concurrency
When not to use:
- If you don’t need thread safety,
Rc
is cheaper
🧪 RefCell<T>
— The Rebel That Allows Interior Mutability (Single-threaded)
What it is:
Mutable borrow checker bypasser — at runtime.
Why use it:
- You want to mutate data through an immutable reference
- You're OK with runtime panics if you mess up borrowing
Example:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
*data.borrow_mut() += 1;
println!("{}", data.borrow());
}
When to use:
- Inside
Rc<T>
when you want shared mutable data
When not to use:
- If you can't guarantee single-threaded access or proper borrow rules
🔒 Mutex<T>
— Lock It Down (Multithreaded Mutability)
What it is:
Mutual exclusion primitive. Allows safe, mutable access from multiple threads — one at a time.
Why use it:
- You want to mutate shared data across threads
- You enjoy waiting in line
Example:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut num = data.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *data.lock().unwrap());
}
When to use:
- When you need interior mutability and thread safety
When not to use:
- When you're in a single-threaded context. Just use
RefCell<T>
📖 RwLock<T>
— Mutex But Smarter
What it is:
A read-write lock. Allows multiple readers OR one writer at a time.
Why use it:
- Read-heavy scenarios
Example:
use std::sync::{Arc, RwLock};
fn main() {
let data = Arc::new(RwLock::new(5));
{
let r1 = data.read().unwrap();
let r2 = data.read().unwrap();
println!("{} and {}", r1, r2);
}
{
let mut w = data.write().unwrap();
*w += 1;
}
println!("{}", *data.read().unwrap());
}
When to use:
- When you're reading way more often than writing
When not to use:
- In write-heavy scenarios — stick to
Mutex
🧩 Frankenstein's Pointer: Rc<RefCell<T>>
& Arc<Mutex<T>>
Sometimes, the best pointer... is two pointers in a trench coat.
Rc<RefCell<T>>
:
- Used for single-threaded shared mutability
- Perfect for GUI state trees or ASTs
Arc<Mutex<T>>
:
- Used for multithreaded shared mutability
- Classic for managing shared state across threads
TL;DR — The Smart Pointer Pokémon Chart:
Use Case | Smart Pointer |
---|---|
Heap allocation | Box<T> |
Shared ownership (single thread) | Rc<T> |
Shared ownership (multi thread) | Arc<T> |
Interior mutability (single) | RefCell<T> |
Interior mutability (multi) | Mutex<T> |
Lots of reads, few writes | RwLock<T> |
Single-thread shared mutability | Rc<RefCell<T>> |
Multi-thread shared mutability | Arc<Mutex<T>> |
🚀 Conclusion
Smart pointers are like Marvel characters — each one has their strengths, weaknesses, and the occasional spin-off series.
Learning when to use which one is half the battle. The other half? Fighting the compiler until you cry tears of borrow of moved value
errors.
So be smart, use smart pointers, and go make Rust proud. 🦀
And again, if you're feeling smart and want to get hands-on with stuff like this, peep my Codecrafters profile here.
Until next time, may your threads be safe and your borrows never dangle.