blog.dopana

Back

Thirteenth post in the intermediate Rust series. While ordinary references (&T) are just borrowed views into memory, Smart Pointers are data structures that own the data they point to. They act like references but carry extra metadata and capabilities — heap allocation, shared ownership, and interior mutability.

graph TD
    A["Smart Pointers"] --> B["Box<T><br/>Heap allocation<br/>Single owner"]
    A --> C["Rc<T><br/>Reference counting<br/>Single-threaded shared"]
    A --> D["RefCell<T><br/>Interior mutability<br/>Runtime borrow checking"]
    A --> E["Arc<T><br/>Atomic reference counting<br/>Thread-safe shared"]
    A --> F["Deref Trait<br/>Customizes * operator"]

What Makes a Pointer “Smart”?#

A regular reference (&T) is just an address — it borrows data owned by someone else. A smart pointer, on the other hand, owns the data it points to. When the smart pointer goes out of scope, the data is automatically dropped and the memory is freed.

The magic ingredient is the Deref trait. It lets smart pointers behave like ordinary references by customizing what happens when you use the * operator.

use std::ops::Deref;

struct MyBox<T>(T);

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

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
rust

With Deref implemented, you can use *my_box just like *reference, and Rust will automatically call deref() behind the scenes.

Box: Putting Data on the Heap#

Box<T> is the simplest smart pointer. It allocates its data on the heap and gives you a single owner. When the Box is dropped, the heap memory is freed.

Why Use Box?#

  1. Recursive types: Types that contain themselves (like linked lists or tree nodes) need indirection to have a known size.
  2. Large data transfer: Moving large data to the heap avoids expensive stack copies.
  3. Trait objects: Storing values of different types behind a common interface.

Recursive Types with Box#

Without Box, a recursive struct like a linked list would have infinite size:

graph LR
    A["Cons(1, Box)"] --> B["Cons(2, Box)"]
    B --> C["Cons(3, Box)"]
    C --> D["Nil"]

Box for Trait Objects#

Box<dyn Trait> lets you store different types behind a single interface:

Rc: Shared Ownership (Single-Threaded)#

Sometimes you need multiple owners of the same data. Rc<T> (Reference Counted) enables this in single-threaded scenarios.

How It Works#

Every time you call Rc::clone(), the reference count goes up. When a clone is dropped, the count goes down. The data is only freed when the count reaches zero.

use std::rc::Rc;

fn main() {
    let data = Rc::new(String::from("Hello, shared world!"));
    println!("Reference count: {}", Rc::strong_count(&data)); // 1

    {
        let data2 = Rc::clone(&data); // count goes up
        println!("Reference count: {}", Rc::strong_count(&data)); // 2
        println!("data2: {data2}");
    } // data2 is dropped, count goes back down

    println!("Reference count: {}", Rc::strong_count(&data)); // 1
    println!("data: {data}");
}
rust

Tree Structures with Rc#

Rc<T> is perfect for tree structures where children need to reference their parent:

[!NOTE] Rc<T> uses non-atomic reference counting, which is faster but not thread-safe. For multi-threaded code, use Arc<T> instead.

RefCell: Interior Mutability#

RefCell<T> enables interior mutability — the ability to mutate data even when you only have an immutable reference to the RefCell itself.

The Borrow Checker at Runtime#

While Rc<T> tracks how many owners exist, RefCell<T> tracks how many references exist at runtime. The rules are the same as compile-time borrowing, but violations cause a runtime panic instead of a compile error.

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(5);

    {
        let mut borrow = cell.borrow_mut(); // mutable borrow
        *borrow += 1;
        println!("Inside block: {borrow}");
    } // borrow is dropped here

    println!("After block: {}", cell.borrow()); // immutable borrow
}
rust

The Rc + RefCell Combo#

Combining Rc<T> and RefCell<T> gives you shared ownership with the ability to mutate:

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

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

    let data2 = Rc::clone(&data);
    data2.borrow_mut().push(4);

    println!("data: {:?}", data.borrow()); // [1, 2, 3, 4]
}
rust

[!WARNING] RefCell<T> panics at runtime if you violate borrow rules (e.g., two mutable borrows at once). This trades compile-time safety for flexibility — use it judiciously.

Arc: Thread-Safe Shared Ownership#

Arc<T> (Atomically Reference Counted) is the multi-threaded version of Rc<T>. It uses atomic operations to safely share data across threads.

graph TD
    A["Arc<Vec>"] --> B["Thread 1<br/>Arc::clone"]
    A --> C["Thread 2<br/>Arc::clone"]
    A --> D["Thread 3<br/>Arc::clone"]
    A --> E["Thread 4<br/>Arc::clone"]

[!TIP] Arc<T> has a small performance cost due to atomic operations. If you don’t need thread safety, Rc<T> is faster.

When to Use Which Smart Pointer?#

Smart PointerUse CaseThread-Safe?
Box<T>Heap allocation, recursive types, trait objects✅ Yes (single owner)
Rc<T>Multiple owners, single-threaded❌ No
RefCell<T>Interior mutability, runtime borrow checking❌ No
Arc<T>Multiple owners, multi-threaded✅ Yes
Rc<RefCell<T>>Shared + mutable, single-threaded❌ No
Arc<Mutex<T>>Shared + mutable, multi-threaded✅ Yes

ELI5 Analogy: The Library Book System#

Think of smart pointers like different ways to manage a library book:

  • Box<T> is like checking out a book — you’re the sole owner. When you return it (drop it), the library reclaims it.
  • Rc<T> is like a book that multiple study groups can borrow. The library keeps track of how many groups have it. Only when the last group returns it does the library reclaim it.
  • RefCell<T> is like a book with a “read-only” cover. You can look at it, but to write notes inside, you need special permission that the librarian (runtime checker) grants — and only one person can write at a time.
  • Arc<T> is like Rc<T> but for a university-wide library system where multiple campuses share the same book. The atomic counter ensures no campus loses track of who has the book.

Summary#

  • Smart Pointers own their data and implement Deref to behave like references.
  • Box<T> provides heap allocation and is essential for recursive types and trait objects.
  • Rc<T> enables multiple ownership in single-threaded code via reference counting.
  • RefCell<T> provides interior mutability with runtime borrow checking.
  • Arc<T> is the thread-safe version of Rc<T> for concurrent code.
  • Combining these (e.g., Rc<RefCell<T>> or Arc<Mutex<T>>) gives you powerful patterns for shared, mutable state.

In the next post, we will explore Functional Rust — closures and iterators — and how Rust’s zero-cost abstractions let you write expressive, high-level code without sacrificing performance.

References#