In the previous tutorial, we learned closures and iterators. Now we learn smart pointers — types that act like pointers but have extra capabilities.

In Rust, the most common pointer is a reference (&T). References borrow data but do not own it. Smart pointers own the data they point to. They also add features like heap allocation, reference counting, and interior mutability.

What Is a Smart Pointer?

A smart pointer is a struct that:

  1. Implements the Deref trait — so it behaves like a reference
  2. Implements the Drop trait — so it cleans up when it goes out of scope

You already know two smart pointers: String (owns heap-allocated text) and Vec<T> (owns a heap-allocated array). In this tutorial, we learn four more: Box<T>, Rc<T>, Arc<T>, and RefCell<T>.

Box<T>: Heap Allocation

Box<T> is the simplest smart pointer. It puts data on the heap instead of the stack:

fn main() {
    let boxed = Box::new(42);
    println!("Value: {}", boxed);       // 42
    println!("Plus one: {}", *boxed + 1); // 43
}

The value 42 is stored on the heap. boxed is a pointer on the stack that points to it. When boxed goes out of scope, the heap memory is freed.

When to Use Box

1. Large data that you do not want to copy:

let big_array = Box::new([0u8; 1000]);
println!("Length: {}", big_array.len());

Without Box, this array lives on the stack. With Box, only an 8-byte pointer is on the stack.

2. Recursive types:

This does not compile:

enum List<T> {
    Cons(T, List<T>),  // ERROR: infinite size
    Nil,
}

The compiler cannot calculate the size of List because it contains itself. Fix it with Box:

#[derive(Debug, PartialEq)]
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}

Now Cons stores a value and a pointer (fixed size) to the next node:

impl<T> List<T> {
    fn new() -> List<T> {
        List::Nil
    }

    fn push(self, value: T) -> List<T> {
        List::Cons(value, Box::new(self))
    }

    fn len(&self) -> usize {
        match self {
            List::Nil => 0,
            List::Cons(_, rest) => 1 + rest.len(),
        }
    }
}

fn main() {
    let list = List::new().push(3).push(2).push(1);
    println!("Length: {}", list.len());  // 3
}

3. Trait objects:

fn create_greeter(formal: bool) -> Box<dyn Fn(&str) -> String> {
    if formal {
        Box::new(|name| format!("Good day, {}.", name))
    } else {
        Box::new(|name| format!("Hey, {}!", name))
    }
}

The Deref Trait

The Deref trait lets you use * on your type, just like a reference. It also enables deref coercion — automatic conversion between reference types.

use std::ops::Deref;

struct MyBox<T>(T);

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

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

Now MyBox works like a reference:

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

fn main() {
    let my_box = MyBox::new(String::from("Alex"));
    greet(&my_box);  // Deref coercion: &MyBox<String> → &String → &str
}

Deref coercion happens automatically when types do not match. The compiler calls deref() as many times as needed to make the types line up.

The Drop Trait

The Drop trait lets you run code when a value goes out of scope. It is like a destructor in C++ or a finalizer in Java:

struct Resource {
    name: String,
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("Dropped: {}", self.name);
    }
}

fn main() {
    let _r1 = Resource { name: String::from("file.txt") };
    let _r2 = Resource { name: String::from("db_conn") };
    println!("Resources created");
}
// Output:
// Resources created
// Dropped: db_conn   ← r2 dropped first (reverse order)
// Dropped: file.txt  ← r1 dropped second

Values are dropped in reverse order of creation. This is important for cleanup — if r2 depends on r1, r2 is cleaned up first.

Early Drop

You can drop a value early with std::mem::drop():

let resource = Resource { name: String::from("temp") };
drop(resource);  // Dropped now, not at end of scope
println!("After drop");

You cannot call resource.drop() directly — Rust prevents double-free by requiring you to use std::mem::drop().

Rc<T>: Reference Counting

Sometimes you need multiple owners for the same data. Ownership rules say one owner, but Rc<T> lets you share:

use std::rc::Rc;

fn main() {
    let data = Rc::new(String::from("shared data"));

    let clone1 = Rc::clone(&data);
    let clone2 = Rc::clone(&data);

    println!("Count: {}", Rc::strong_count(&data));  // 3
    println!("data: {}", data);
    println!("clone1: {}", clone1);
    println!("clone2: {}", clone2);

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

Rc::clone() does not copy the data. It only increments the reference count. The data is freed when the last Rc is dropped and the count reaches zero.

Rc for Shared Tree Nodes

Rc is perfect for tree structures where nodes can have multiple parents:

use std::rc::Rc;

struct TreeNode {
    value: i32,
    children: Vec<Rc<TreeNode>>,
}

impl TreeNode {
    fn new(value: i32) -> TreeNode {
        TreeNode { value, children: Vec::new() }
    }

    fn with_children(value: i32, children: Vec<Rc<TreeNode>>) -> TreeNode {
        TreeNode { value, children }
    }

    fn sum(&self) -> i32 {
        let children_sum: i32 = self.children.iter().map(|c| c.sum()).sum();
        self.value + children_sum
    }
}

fn main() {
    // Shared leaf — used by two branches
    let leaf = Rc::new(TreeNode::new(10));
    let branch1 = Rc::new(TreeNode::with_children(5, vec![Rc::clone(&leaf)]));
    let branch2 = Rc::new(TreeNode::with_children(3, vec![Rc::clone(&leaf)]));
    let root = TreeNode::with_children(1, vec![branch1, branch2]);

    println!("Sum: {}", root.sum());  // 1 + 5 + 10 + 3 + 10 = 29
    println!("Leaf count: {}", Rc::strong_count(&leaf));  // 3
}

Important: Rc<T> is not thread-safe. It only works in single-threaded code. For multi-threaded code, use Arc<T>.

RefCell<T>: Interior Mutability

Normally, you cannot mutate data behind a shared reference (&T). RefCell<T> lets you break this rule at runtime:

use std::cell::RefCell;

struct Counter {
    count: RefCell<u32>,
    name: String,
}

impl Counter {
    fn new(name: &str) -> Counter {
        Counter {
            count: RefCell::new(0),
            name: name.to_string(),
        }
    }

    fn increment(&self) {  // Takes &self, not &mut self
        *self.count.borrow_mut() += 1;
    }

    fn get(&self) -> u32 {
        *self.count.borrow()
    }
}

fn main() {
    let counter = Counter::new("clicks");
    counter.increment();  // No &mut needed!
    counter.increment();
    counter.increment();
    println!("{}: {}", counter.name, counter.get());  // clicks: 3
}

RefCell moves the borrowing rules from compile time to runtime:

  • borrow() — returns a shared reference (like &T)
  • borrow_mut() — returns a mutable reference (like &mut T)

If you violate the rules (two mutable borrows at the same time), the program panics at runtime instead of failing at compile time.

Rc<RefCell<T>>: Shared Mutable Data

The most common pattern is combining Rc and RefCell:

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

fn main() {
    let data = Rc::new(RefCell::new(vec![String::from("initial")]));

    let handle1 = Rc::clone(&data);
    let handle2 = Rc::clone(&data);

    handle1.borrow_mut().push(String::from("from handle1"));
    handle2.borrow_mut().push(String::from("from handle2"));

    println!("{:?}", data.borrow());
    // ["initial", "from handle1", "from handle2"]
}

Both handle1 and handle2 can read and write the shared Vec. This is safe because RefCell checks the borrowing rules at runtime.

Arc<T>: Thread-Safe Reference Counting

Arc<T> is Rc<T> for multi-threaded code. “Arc” stands for Atomic Reference Counting. It uses atomic operations to safely update the count from multiple threads:

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

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    let data_clone = Arc::clone(&data);

    let handle = thread::spawn(move || {
        let sum: i32 = data_clone.iter().sum();
        println!("Sum from thread: {}", sum);
    });

    println!("Original: {:?}", data);
    handle.join().unwrap();
}

Arc is slightly slower than Rc because atomic operations have overhead. Use Rc when you do not need threads. Use Arc when you do.

Arc<Mutex<T>>: Thread-Safe Shared Mutable Data

For mutable shared data across threads, combine Arc with Mutex (which we cover in the next tutorial):

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

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

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

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

When to Use Which

TypeOwnershipThread-SafeMutableUse Case
Box<T>Single ownerYesIf owned mutablyHeap allocation, recursive types
Rc<T>Multiple ownersNoNo (use RefCell)Shared data, single thread
Arc<T>Multiple ownersYesNo (use Mutex)Shared data, multiple threads
RefCell<T>Single ownerNoYes (runtime checked)Interior mutability
Rc<RefCell<T>>Multiple ownersNoYesShared mutable data, single thread
Arc<Mutex<T>>Multiple ownersYesYesShared mutable data, multiple threads

Start simple. Use owned values and references first. Reach for smart pointers only when you need them.

Common Mistakes

Mistake: Using Rc in Multi-Threaded Code

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

let data = Rc::new(42);
thread::spawn(move || {
    println!("{}", data);  // ERROR: Rc<i32> is not Send
});

Fix: Use Arc for threads:

use std::sync::Arc;
let data = Arc::new(42);

Mistake: Two Mutable Borrows on RefCell

let cell = RefCell::new(42);
let borrow1 = cell.borrow_mut();
let borrow2 = cell.borrow_mut();  // PANIC at runtime!

RefCell checks borrowing rules at runtime. Two mutable borrows at the same time cause a panic. Fix: Drop the first borrow before creating the second:

let cell = RefCell::new(42);
{
    let mut borrow1 = cell.borrow_mut();
    *borrow1 += 1;
}  // borrow1 dropped here
{
    let mut borrow2 = cell.borrow_mut();
    *borrow2 += 1;
}

Mistake: Reference Cycles with Rc

Rc uses reference counting. If two Rc values point to each other, the count never reaches zero and the memory leaks:

// A -> B -> A -> B -> ... (cycle — memory leak!)

Fix: Use Weak<T> (a weak reference that does not increment the count) for back-references. This is important for tree structures where children point back to parents.

Decision Flowchart

Here is a simple guide:

Start with: owned values (String, Vec<T>, plain structs)

Need a reference? Use &T or &mut T

Need heap allocation? Use Box<T>

Need multiple owners, single thread? Use Rc<T>

Need multiple owners, multi thread? Use Arc<T>

Need to mutate shared data, single thread? Use Rc<RefCell<T>>

Need to mutate shared data, multi thread? Use Arc<Mutex<T>>

Most Rust programs only use Box and occasionally Arc<Mutex<T>>. Do not reach for Rc or RefCell unless you have a specific reason. Simple ownership is almost always better.

  1. Need heap allocation or recursive types? Use Box<T>.
  2. Need multiple owners? Use Rc<T> (single thread) or Arc<T> (multi thread).
  3. Need to mutate through shared references? Add RefCell<T> or Mutex<T>.

Summary

ConceptWhat It Does
Box<T>Heap-allocates a value, single owner
Deref traitMakes a type behave like a reference
Drop traitRuns cleanup when a value is dropped
Rc<T>Reference-counted shared ownership (single thread)
Arc<T>Atomic reference-counted shared ownership (multi thread)
RefCell<T>Interior mutability with runtime borrow checking
Rc<RefCell<T>>Shared mutable data (single thread)

Source Code

View source code on GitHub ->

What’s Next?

We now understand smart pointers and when to use each one. Next, we learn concurrency — threads, channels, and mutexes. We will see how Arc<Mutex<T>> works in practice, and why Rust’s ownership system makes concurrent programming safe.

Next: Rust Tutorial #14: Concurrency