How do I work with smart pointers in Rust?

Walkthrough

Smart pointers in Rust are data structures that act like pointers but also have additional metadata and capabilities. Unlike regular references, smart pointers own the data they point to and can manage resources automatically.

Rust provides several smart pointer types in the standard library:

  • Box<T> — Heap allocation for single ownership
  • Rc<T> — Reference counting for multiple ownership (single-threaded)
  • Arc<T> — Atomic reference counting for multiple ownership (thread-safe)
  • RefCell<T> — Interior mutability with runtime borrow checking
  • Cell<T> — Interior mutability for Copy types
  • Mutex<T> and RwLock<T> — Interior mutability with locking for concurrency

Smart pointers implement the Deref and Drop traits, which allow them to:

  • Be used like regular references (Deref)
  • Automatically clean up resources when going out of scope (Drop)

Understanding when to use each type is crucial for effective Rust programming.

Code Examples

Using Box for Heap Allocation

// Box<T> allocates data on the heap
// Useful for recursive types and large data
 
// Recursive type example: a cons list
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}
 
use List::{Cons, Nil};
 
fn main() {
    // Box enables recursive types by providing fixed size
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("{:?}", list);
    
    // Box for large data you don't want to copy
    let large_data = Box::new([0u8; 1024 * 1024]); // 1MB on heap
    
    // Box for trait objects (dynamic dispatch)
    let animal: Box<dyn Animal> = Box::new(Dog);
    animal.make_sound();
}
 
trait Animal {
    fn make_sound(&self);
}
 
struct Dog;
impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
}

Using Rc for Multiple Ownership

use std::rc::Rc;
 
// Rc<T> enables multiple ownership (single-threaded only)
// Reference counting tracks how many references exist
 
#[derive(Debug)]
struct Node {
    value: i32,
    next: Option<Rc<Node>>,
}
 
fn main() {
    // Create a node that can be shared
    let a = Rc::new(Node { value: 5, next: None });
    println!("Reference count after creating a: {}", Rc::strong_count(&a)); // 1
    
    // Clone the Rc (increments reference count, doesn't copy data)
    let b = Rc::clone(&a);
    println!("Reference count after cloning to b: {}", Rc::strong_count(&a)); // 2
    
    let c = Rc::clone(&a);
    println!("Reference count after cloning to c: {}", Rc::strong_count(&a)); // 3
    
    // When b and c go out of scope, count decreases
    // When count reaches 0, data is dropped
    
    // Multiple lists can share the same tail
    let shared_tail = Rc::new(Node { value: 10, next: None });
    
    let list1 = Node { value: 1, next: Some(Rc::clone(&shared_tail)) };
    let list2 = Node { value: 2, next: Some(Rc::clone(&shared_tail)) };
    // Both list1 and list2 point to the same shared_tail
}

Using Arc for Thread-Safe Multiple Ownership

use std::sync::Arc;
use std::thread;
 
// Arc<T> is thread-safe reference counting
// Use Arc when sharing data across threads
 
fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    println!("Initial reference count: {}", Arc::strong_count(&data));
    
    let mut handles = vec![];
    
    for i in 0..3 {
        // Clone the Arc for each thread
        let data_clone = Arc::clone(&data);
        
        let handle = thread::spawn(move || {
            println!("Thread {}: data = {:?}", i, data_clone);
            println!("Thread {}: ref count = {}", i, Arc::strong_count(&data_clone));
        });
        
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final reference count: {}", Arc::strong_count(&data));
}

Interior Mutability with RefCell

use std::cell::RefCell;
 
// RefCell<T> allows mutation through immutable references
// Borrow checking happens at runtime instead of compile time
 
fn main() {
    let data = RefCell::new(5);
    
    // Borrow immutably
    let value = *data.borrow();
    println!("Current value: {}", value);
    
    // Borrow mutably and modify
    *data.borrow_mut() += 10;
    println!("Modified value: {}", data.borrow());
    
    // Common pattern: combine with Rc for mutable shared data
    let shared_data = RefCell::new(vec![1, 2, 3]);
    
    // Can modify through an immutable reference
    shared_data.borrow_mut().push(4);
    println!("Shared data: {:?}", shared_data.borrow());
    
    // Runtime panic on invalid borrows
    // This would panic at runtime:
    // let a = data.borrow_mut();
    // let b = data.borrow_mut(); // panic: already borrowed
}

Combining Rc and RefCell

use std::rc::Rc;
use std::cell::RefCell;
 
// Rc<RefCell<T>> is a common pattern for mutable shared data
#[derive(Debug)]
struct Graph {
    value: i32,
    edges: Vec<Rc<RefCell<Graph>>>,
}
 
fn main() {
    let node1 = Rc::new(RefCell::new(Graph { value: 1, edges: vec![] }));
    let node2 = Rc::new(RefCell::new(Graph { value: 2, edges: vec![] }));
    let node3 = Rc::new(RefCell::new(Graph { value: 3, edges: vec![] }));
    
    // Create edges (multiple nodes can point to same node)
    node1.borrow_mut().edges.push(Rc::clone(&node3));
    node2.borrow_mut().edges.push(Rc::clone(&node3));
    
    println!("Node 1: {:?}", node1.borrow().value);
    println!("Node 1 edges: {}", node1.borrow().edges.len());
    println!("Node 3 is referenced {} times", Rc::strong_count(&node3));
}

Using Cell for Copy Types

use std::cell::Cell;
 
// Cell<T> provides interior mutability for Copy types
// No runtime borrow checking needed
 
fn main() {
    let counter = Cell::new(0);
    
    // Get and set operations
    counter.set(counter.get() + 1);
    println!("Counter: {}", counter.get());
    
    // Cell works with any Copy type
    let flag = Cell::new(false);
    flag.set(true);
    
    // Useful for simple state tracking
    struct Config {
        debug_mode: Cell<bool>,
        max_connections: Cell<u32>,
    }
    
    let config = Config {
        debug_mode: Cell::new(false),
        max_connections: Cell::new(10),
    };
    
    // Modify without needing &mut
    config.debug_mode.set(true);
    config.max_connections.set(100);
}

Thread-Safe Interior Mutability with Mutex

use std::sync::{Arc, Mutex};
use std::thread;
 
// Arc<Mutex<T>> for thread-safe mutable shared state
 
fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // Lock the mutex to get mutable access
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
            // Lock is automatically released when num goes out of scope
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final counter: {}", *counter.lock().unwrap());
    
    // Protecting complex data
    let shared_vec = Arc::new(Mutex::new(vec![]));
    
    {
        let mut vec = shared_vec.lock().unwrap();
        vec.push(1);
        vec.push(2);
    }
    
    println!("Shared vector: {:?}", shared_vec.lock().unwrap());
}

Using RwLock for Read-Heavy Workloads

use std::sync::{Arc, RwLock};
use std::thread;
 
// RwLock allows multiple readers OR single writer
// Better than Mutex when reads are more frequent than writes
 
fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3]));
    
    // Multiple readers can hold the lock simultaneously
    {
        let r1 = data.read().unwrap();
        let r2 = data.read().unwrap();
        println!("Reader 1: {:?}", *r1);
        println!("Reader 2: {:?}", *r2);
    }
    
    // Writer needs exclusive access
    {
        let mut w = data.write().unwrap();
        w.push(4);
        w.push(5);
    }
    
    // Spawn reader threads
    let mut handles = vec![];
    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let reader = data_clone.read().unwrap();
            println!("Thread {} read: {:?}", i, *reader);
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Weak References to Prevent Cycles

use std::rc::{Rc, Weak};
use std::cell::RefCell;
 
// Weak<T> creates non-owning references
// Prevents reference cycles that would cause memory leaks
 
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,   // Weak reference to parent
    children: RefCell<Vec<Rc<Node>>>, // Strong references to children
}
 
fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
    
    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
    
    // Set parent as weak reference (doesn't increase strong count)
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
    
    // Access parent through weak reference
    if let Some(parent) = leaf.parent.borrow().upgrade() {
        println!("Leaf parent value: {}", parent.value);
    }
    
    println!("Leaf strong count: {}", Rc::strong_count(&leaf));
    println!("Branch strong count: {}", Rc::strong_count(&branch));
}

Summary

Smart Pointer Ownership Thread Safety Use Case
Box<T> Single Yes Heap allocation, recursive types, trait objects
Rc<T> Multiple No (single-threaded) Shared ownership in single-threaded code
Arc<T> Multiple Yes Shared ownership across threads
RefCell<T> Single with interior mutability No Runtime borrow checking, mutation through immutable reference
Cell<T> Single with interior mutability No Simple Copy types that need mutation
Mutex<T> Single with locking Yes Thread-safe mutable state
RwLock<T> Single with locking Yes Read-heavy concurrent access

Key Points:

  • Use Box for heap allocation and recursive types
  • Use Rc/Arc when multiple owners need to share data
  • Use RefCell/Cell for interior mutability
  • Combine patterns: Rc<RefCell<T>> for mutable shared data (single-threaded)
  • Combine patterns: Arc<Mutex<T>> for mutable shared data (multi-threaded)
  • Use Weak references to prevent reference cycles