TechLead
Lesson 12 of 28
5 min read
Rust

Smart Pointers

Understand Rust smart pointers: Box, Rc, Arc, RefCell, interior mutability, Deref and Drop traits, and when to use each pointer type.

What Are Smart Pointers?

Smart pointers are data structures that act like pointers but also have additional metadata and capabilities. They own the data they point to and manage its lifecycle. The most common smart pointers in Rust are Box, Rc, Arc, and RefCell.

Box<T> — Heap Allocation

fn main() {
    // Box puts data on the heap
    let b = Box::new(5);
    println!("b = {b}"); // Deref makes it transparent

    // Use cases for Box:
    // 1. Recursive types (unknown size at compile time)
    // 2. Large data you want on the heap
    // 3. Trait objects

    // Recursive type example
    #[derive(Debug)]
    enum List {
        Cons(i32, Box),
        Nil,
    }

    let list = List::Cons(1,
        Box::new(List::Cons(2,
            Box::new(List::Cons(3,
                Box::new(List::Nil))))));
    println!("{:?}", list);

    // Large struct on heap to avoid stack overflow
    let big_array = Box::new([0u8; 1_000_000]);
    println!("Big array len: {}", big_array.len());
}

// Trait objects with Box
trait Animal {
    fn speak(&self) -> &str;
}

struct Dog;
struct Cat;

impl Animal for Dog { fn speak(&self) -> &str { "Woof!" } }
impl Animal for Cat { fn speak(&self) -> &str { "Meow!" } }

fn get_animal(kind: &str) -> Box {
    match kind {
        "dog" => Box::new(Dog),
        _ => Box::new(Cat),
    }
}

Rc<T> — Reference Counting

use std::rc::Rc;

fn main() {
    // Rc allows multiple owners of the same data
    // (single-threaded only!)
    let data = Rc::new(vec![1, 2, 3]);
    println!("Reference count: {}", Rc::strong_count(&data)); // 1

    let clone1 = Rc::clone(&data); // Increment count, NOT deep copy
    println!("Reference count: {}", Rc::strong_count(&data)); // 2

    let clone2 = Rc::clone(&data);
    println!("Reference count: {}", Rc::strong_count(&data)); // 3

    // All three variables point to the same data
    println!("{:?}", data);
    println!("{:?}", clone1);
    println!("{:?}", clone2);

    drop(clone2);
    println!("After drop: {}", Rc::strong_count(&data)); // 2

    // Rc is read-only! You can't mutate the data.
    // For mutation, combine with RefCell (next section)
}

RefCell<T> — Interior Mutability

Interior Mutability Pattern

RefCell<T> enforces borrowing rules at runtime instead of compile time. This lets you mutate data even when there are immutable references to it, as long as the rules are respected at runtime (or it panics).

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    // borrow() — immutable borrow (checked at runtime)
    println!("{:?}", data.borrow());

    // borrow_mut() — mutable borrow (checked at runtime)
    data.borrow_mut().push(4);
    println!("{:?}", data.borrow());

    // Combining Rc + RefCell = multiple owners with mutation
    let shared = Rc::new(RefCell::new(0));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    *a.borrow_mut() += 10;
    *b.borrow_mut() += 20;
    println!("Shared value: {}", shared.borrow()); // 30

    // PANIC if you violate borrowing rules at runtime:
    // let borrow1 = data.borrow();
    // let borrow2 = data.borrow_mut(); // PANIC: already borrowed
}

Arc<T> — Atomic Reference Counting

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc = Rc but thread-safe (uses atomic operations)
    let data = Arc::new(Mutex::new(0));

    let mut handles = vec![];
    for _ in 0..10 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let mut num = data.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *data.lock().unwrap()); // 10
}

// When to use which:
// Box    — Single owner, heap allocation
// Rc     — Multiple owners, single-threaded
// Arc    — Multiple owners, multi-threaded
// RefCell — Interior mutability (runtime borrow checking)
// Mutex  — Interior mutability (thread-safe)

Deref and Drop Traits

use std::ops::Deref;

// Custom smart pointer
struct MyBox(T);

impl MyBox {
    fn new(x: T) -> MyBox {
        MyBox(x)
    }
}

impl Deref for MyBox {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("Dropping MyBox with value: {:?}", self.0);
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let x = MyBox::new(String::from("Rust"));

    // Deref coercion: &MyBox -> &String -> &str
    hello(&x);

    // Explicit early drop
    let y = MyBox::new(42);
    drop(y); // Calls Drop::drop immediately
    println!("y has been dropped");
} // x is dropped here automatically

Key Takeaways

  • Box<T> allocates on the heap — essential for recursive types and trait objects
  • Rc<T> enables multiple ownership via reference counting (single-threaded)
  • Arc<T> is the thread-safe version of Rc using atomic operations
  • RefCell<T> moves borrow checking to runtime, enabling interior mutability
  • Deref enables transparent dereferencing; Drop runs cleanup code automatically

Continue Learning