Web Analytics

Smart Pointers

Advanced ~35 min read

Smart pointers are data structures that act like pointers but have additional metadata and capabilities. Unlike regular references, smart pointers own the data they point to. Rust's smart pointers enable advanced patterns like heap allocation, reference counting, and interior mutability.

What Are Smart Pointers?

A pointer is a variable that contains an address in memory. In Rust, the most common pointer is a reference (&). Smart pointers are data structures that not only act like pointers but also have additional metadata and capabilities.

Key Difference: References borrow data, while smart pointers own the data they point to. Smart pointers are usually implemented using structs and implement the Deref and Drop traits.
Rust Smart Pointers

Common Smart Pointer Types

Type Purpose Use Case
Box<T> Heap allocation Recursive types, large data, dynamic dispatch
Rc<T> Reference counting Multiple ownership (single-threaded)
RefCell<T> Interior mutability Mutate data with immutable references
Arc<T> Atomic reference counting Multiple ownership (multi-threaded)

Box<T> - Heap Allocation

Box<T> is the most straightforward smart pointer. It allows you to store data on the heap rather than the stack.

Output
Click Run to execute your code

When to Use Box<T>

  • Recursive types: Types whose size can't be known at compile time
  • Large data: Transfer ownership without copying large amounts of data
  • Trait objects: When you want a value with a type implementing a specific trait
Pro Tip: Box has minimal overhead - just the cost of heap allocation. Use it when you need heap storage with single ownership semantics.

Rc<T> - Reference Counting

Rc<T> enables multiple ownership by keeping track of the number of references to a value. The value is dropped when the last reference goes out of scope.

Output
Click Run to execute your code
Important: Rc<T> is only for single-threaded scenarios. Use Arc<T> (Atomic Rc) for multi-threaded programs.

Rc Methods

Method Description
Rc::new(value) Create new Rc pointer
Rc::clone(&rc) Create new reference (shallow copy)
Rc::strong_count(&rc) Get number of strong references
Rc::weak_count(&rc) Get number of weak references

RefCell<T> - Interior Mutability

RefCell<T> provides interior mutability - a design pattern that allows you to mutate data even when there are immutable references to that data.

Output
Click Run to execute your code
Borrowing Rules at Runtime: With RefCell<T>, borrowing rules are enforced at runtime instead of compile time. If you violate the rules, your program will panic.

RefCell Methods

Method Returns Description
borrow() Ref<T> Immutable borrow (panics if mutably borrowed)
borrow_mut() RefMut<T> Mutable borrow (panics if already borrowed)
try_borrow() Result<Ref<T>> Safe immutable borrow
try_borrow_mut() Result<RefMut<T>> Safe mutable borrow

Combining Rc<T> and RefCell<T>

A common Rust pattern is combining Rc<T> and RefCell<T> to have multiple owners with the ability to mutate data.

Output
Click Run to execute your code
Pattern: Rc<RefCell<T>> is useful for graph-like data structures where nodes need multiple owners and mutability.

The Deref Trait

The Deref trait allows you to customize the behavior of the dereference operator *. Smart pointers implement Deref so they can be treated like regular references.

Output
Click Run to execute your code

Deref Coercion

Deref coercion automatically converts a reference to a type implementing Deref into a reference to another type. This happens automatically when you pass a reference as an argument to a function.

Example: &String can be coerced to &str because String implements Deref<Target=str>.

The Drop Trait

The Drop trait lets you customize what happens when a value goes out of scope. Smart pointers use Drop to clean up resources.

Output
Click Run to execute your code
Important: You can't call drop() method directly. Use std::mem::drop() to drop a value early.

Best Practices

  • Prefer ownership: Use smart pointers only when you need their specific capabilities
  • Use Box for simple heap allocation: When you just need data on the heap
  • Use Rc for shared ownership: When multiple parts of code need to read the same data
  • Use RefCell sparingly: Runtime borrowing checks have overhead and can panic
  • Avoid cycles: Rc creates cycles that leak memory. Use Weak references
  • Document interior mutability: Make it clear when using RefCell in your APIs

Common Mistakes

1. Creating reference cycles with Rc

Wrong:

// This creates a memory leak!
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    next: Option<Rc<RefCell<Node>>>,
}

let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));
a.borrow_mut().next = Some(Rc::clone(&b));  // Cycle!

Fix - Use Weak references:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    next: Option<Rc<RefCell<Node>>>,
    prev: Option<Weak<RefCell<Node>>>,  // Use Weak
}

2. Multiple mutable borrows with RefCell

Wrong:

let value = RefCell::new(5);
let borrow1 = value.borrow_mut();
let borrow2 = value.borrow_mut();  // Panic at runtime!

Fix - Drop borrows when done:

let value = RefCell::new(5);
{
    let mut borrow1 = value.borrow_mut();
    *borrow1 = 10;
}  // borrow1 dropped here
let mut borrow2 = value.borrow_mut();  // OK!

3. Using Rc in multi-threaded code

Wrong:

use std::rc::Rc;
use std::thread;

let data = Rc::new(5);
thread::spawn(move || {  // Error: Rc is not Send
    println!("{}", data);
});

Fix - Use Arc instead:

use std::sync::Arc;
use std::thread;

let data = Arc::new(5);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
    println!("{}", data_clone);
});

Exercise: Smart Pointers Practice

Task: Implement a simple graph structure using Rc and RefCell to handle shared ownership and mutability.

Requirements:

  • Create nodes that can have multiple neighbors
  • Use Rc<RefCell<Node>> for shared mutable ownership
  • Implement the ability to add neighbors to nodes
  • Create a graph with cycles (node A points to B, B to C, C back to A)
Output
Click Run to execute your code
Show Solution
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    neighbors: RefCell<Vec<Rc<RefCell<Node>>>>,
}

impl Node {
    fn new(value: i32) -> Rc<RefCell<Node>> {
        Rc::new(RefCell::new(Node {
            value,
            neighbors: RefCell::new(vec![]),
        }))
    }

    fn add_neighbor(&mut self, neighbor: Rc<RefCell<Node>>) {
        self.neighbors.borrow_mut().push(neighbor);
    }
}

fn main() {
    let node1 = Node::new(1);
    let node2 = Node::new(2);
    let node3 = Node::new(3);

    // Connect the nodes in a cycle
    node1.borrow_mut().add_neighbor(Rc::clone(&node2));
    node2.borrow_mut().add_neighbor(Rc::clone(&node3));
    node3.borrow_mut().add_neighbor(Rc::clone(&node1));

    println!("Graph created with cycles!");
    println!("Node 1 has {} neighbors", node1.borrow().neighbors.borrow().len());
}

Summary

  • Smart pointers own the data they point to, unlike references
  • Box<T> stores data on the heap with single ownership
  • Rc<T> enables multiple ownership via reference counting
  • RefCell<T> allows interior mutability with runtime borrow checks
  • Rc<RefCell<T>> combines shared ownership with mutability
  • Deref trait allows smart pointers to act like references
  • Drop trait customizes cleanup when values go out of scope
  • Arc<T> is the thread-safe version of Rc
  • Weak<T> prevents reference cycles
  • Smart pointers enable advanced patterns while maintaining memory safety

What's Next?

Now that you understand smart pointers, you're ready to learn about Packages, Crates, and Modules - Rust's module system for organizing code into reusable components and managing project structure.