Smart Pointers in Rust: A Love Story Between You and Memory Safety 💘
PRANTA Dutta

PRANTA Dutta @pranta

About: I'm a full-stack developer with 3 years of experience. My focus is Flutter & React Native.

Location:
Chattogram, Bangladesh
Joined:
Dec 17, 2020

Smart Pointers in Rust: A Love Story Between You and Memory Safety 💘

Publish Date: Aug 1
0 0

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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.

Comments 0 total

    Add comment