Skip to content

Rust Smart Pointers — Box, Rc, Arc & RefCell

DodaTech Updated 2026-06-21 6 min read

Rust smart pointers extend the language with heap allocation, reference counting, shared ownership, and interior mutability, all while maintaining memory safety through the borrow checker and Drop semantics.

What You'll Learn

In this tutorial, you'll learn how Rust smart pointers work: Box<T> for heap allocation, Rc<T> for single-threaded reference counting, Arc<T> for thread-safe sharing, RefCell<T> for interior mutability, and how to choose the right pointer.

Why It Matters

Systems programming often requires shared ownership, mutable access through shared references, or recursion through heap-allocated types. Smart pointers make these patterns safe and ergonomic without requiring a garbage collector.

Real-World Use

GUI frameworks use Rc<RefCell<T>> for shared mutable widget state. Web servers use Arc<T> to share configuration across threads. Graph data structures use Rc and Weak for parent-child relationships. Durga Antivirus Pro uses Arc to share threat signature databases across hundreds of concurrent scan threads.

flowchart TD
    subgraph "Single Owner"
        B[Box] -->|Unique ownership| H1[Heap data]
    end
    subgraph "Single Thread"
        R[Rc] -->|Shared ownership| H2[Heap data]
        RC[RefCell] -->|Runtime borrow check| M[Mutable access]
    end
    subgraph "Multi Thread"
        A[Arc] -->|Atomic ref counting| H3[Shared data]
        MUT[Arc>] -->|Sync mutability| H4[Thread-safe access]
    end
â„šī¸ Info

Prerequisites: Rust Ownership, Borrowing, and Traits & Generics.

Box for Heap Allocation

Box<T> is the simplest smart pointer. It allocates data on the heap and provides unique ownership.

#[derive(Debug)]
enum BinaryTree {
    Node(i32, Box<BinaryTree>, Box<BinaryTree>),
    Leaf,
}

fn tree_sum(tree: &BinaryTree) -> i32 {
    match tree {
        BinaryTree::Node(val, left, right) => {
            val + tree_sum(left) + tree_sum(right)
        }
        BinaryTree::Leaf => 0,
    }
}

fn main() {
    let tree = BinaryTree::Node(
        10,
        Box::new(BinaryTree::Node(5, Box::new(BinaryTree::Leaf), Box::new(BinaryTree::Leaf))),
        Box::new(BinaryTree::Node(15, Box::new(BinaryTree::Leaf), Box::new(BinaryTree::Leaf))),
    );
    println!("Tree sum: {}", tree_sum(&tree));
    println!("Tree: {:?}", tree);
}

Expected output:

Tree sum: 30
Tree: Node(10, Node(5, Leaf, Leaf), Node(15, Leaf, Leaf))

Rc for Reference Counting

Rc<T> enables multiple owners of the same data within a single thread.

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3, 4, 5]);
    println!("Initial ref count: {}", Rc::strong_count(&data));

    {
        let shared1 = Rc::clone(&data);
        let shared2 = Rc::clone(&data);
        println!("Inside scope: {}", Rc::strong_count(&data));
        println!("shared1: {:?}", shared1);
        println!("shared2: {:?}", shared2);
    } // shared1 and shared2 dropped here

    println!("After scope: {}", Rc::strong_count(&data));
    println!("Original still valid: {:?}", data);
}

Expected output:

Initial ref count: 1
Inside scope: 3
shared1: vec![1, 2, 3, 4, 5]
shared2: vec![1, 2, 3, 4, 5]
After scope: 1
Original still valid: vec![1, 2, 3, 4, 5]

Arc for Thread-Safe Sharing

Arc<T> is like Rc<T> but uses atomic operations for thread safety.

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

fn main() {
    let config = Arc::new(String::from("shared_config"));
    let mut handles = vec![];

    for i in 0..5 {
        let cfg = Arc::clone(&config);
        let handle = thread::spawn(move || {
            println!("Thread {} sees config: {}", i, cfg);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
    println!("Main sees config: {}", config);
}

Expected output:

Thread 0 sees config: shared_config
Thread 1 sees config: shared_config
Thread 2 sees config: shared_config
Thread 3 sees config: shared_config
Thread 4 sees config: shared_config
Main sees config: shared_config

RefCell for Interior Mutability

RefCell<T> allows mutation through shared references, with runtime borrow checking.

use std::cell::RefCell;

struct Cache {
    data: RefCell<Vec<String>>,
}

impl Cache {
    fn new() -> Self {
        Cache { data: RefCell::new(vec![]) }
    }

    fn get(&self, index: usize) -> Option<String> {
        let data = self.data.borrow();
        data.get(index).cloned()
    }

    fn push(&self, item: String) {
        let mut data = self.data.borrow_mut();
        data.push(item);
    }

    fn len(&self) -> usize {
        self.data.borrow().len()
    }
}

fn main() {
    let cache = Cache::new();
    cache.push("item1".into());
    cache.push("item2".into());
    cache.push("item3".into());

    println!("Cache length: {}", cache.len());
    println!("Item at 1: {:?}", cache.get(1));
    println!("All items accessible through shared reference");
}

Expected output:

Cache length: 3
Item at 1: Some("item2")
All items accessible through shared reference

Combining Rc and RefCell

The Rc<RefCell<T>> pattern enables shared mutable state.

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

#[derive(Debug)]
struct Task {
    id: u32,
    description: String,
    completed: RefCell<bool>,
}

fn main() {
    let task = Rc::new(Task {
        id: 1,
        description: String::from("Learn smart pointers"),
        completed: RefCell::new(false),
    });

    let reference1 = Rc::clone(&task);
    let reference2 = Rc::clone(&task);

    // Mark complete through shared reference
    *reference1.completed.borrow_mut() = true;

    println!("Task: {:?}", reference2);
    println!("Completed: {}", reference2.completed.borrow());
    println!("Ref count: {}", Rc::strong_count(&task));
}

Expected output:

Task: Task { id: 1, description: "Learn smart pointers", completed: RefCell { value: true } }
Completed: true
Ref count: 3

Common Mistakes

1. Reference Cycles with Rc

Using Rc without Weak creates reference cycles that never free memory. Use Weak for parent references to break cycles.

2. Runtime Borrow Panics with RefCell

RefCell panics if you borrow mutably while already borrowed. Check borrow state with try_borrow_mut() to avoid panics.

3. Using Rc Across Threads

Rc is not Send. Use Arc for multi-threaded scenarios.

4. Overusing Smart Pointers

Not every allocation needs a smart pointer. Use regular references (&T) for borrowing and Box only when heap allocation is required.

5. Forgetting Weak for Trees

In tree/graph structures, child-to-parent references should use Weak to prevent reference cycles.

Practice Questions

1. What is the difference between Box, Rc, and Arc? Box provides unique ownership on the heap. Rc provides shared ownership within a thread using non-atomic reference counting. Arc provides thread-safe shared ownership using atomic reference counting.

2. What does RefCell enable? RefCell enables interior mutability — mutation through shared references. Borrow rules are enforced at runtime instead of compile time, panicking on violation.

3. Why would you use Weak? Weak holds a non-owning reference to prevent reference cycles. Use Weak::upgrade() to get an Option<Rc<T>>. If the value has been dropped, you get None.

4. When would you use Rc<RefCell>? For shared mutable state within a single thread, like a UI widget tree or shared configuration that needs temporary mutation.

5. Challenge: Implement a graph where nodes are Rc<RefCell<Node>> and edges use Weak references to prevent cycles. Implement a traversal method.

Mini Project: Shared Task Manager

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

#[derive(Debug)]
struct SharedTask {
    id: u32,
    description: String,
    completed: RefCell<bool>,
}

struct TaskManager {
    tasks: RefCell<Vec<Rc<SharedTask>>>,
}

impl TaskManager {
    fn new() -> Self {
        TaskManager { tasks: RefCell::new(vec![]) }
    }

    fn add_task(&self, id: u32, description: &str) -> Rc<SharedTask> {
        let task = Rc::new(SharedTask {
            id,
            description: description.to_string(),
            completed: RefCell::new(false),
        });
        self.tasks.borrow_mut().push(Rc::clone(&task));
        task
    }

    fn complete_task(&self, id: u32) {
        for task in self.tasks.borrow().iter() {
            if task.id == id {
                *task.completed.borrow_mut() = true;
                println!("Task {} completed", id);
                return;
            }
        }
        println!("Task {} not found", id);
    }

    fn print_status(&self) {
        for task in self.tasks.borrow().iter() {
            let status = if *task.completed.borrow() { "Done" } else { "Pending" };
            println!("[{}] Task {}: {} ({})",
                     status, task.id, task.description, Rc::strong_count(task));
        }
    }
}

fn main() {
    let manager = TaskManager::new();
    let t1 = manager.add_task(1, "Learn Box");
    let t2 = manager.add_task(2, "Learn Rc");
    manager.print_status();
    manager.complete_task(1);
    manager.print_status();
    println!("t1 ref after completion: {:?}", t1);
}

FAQ

What is interior mutability?

Interior mutability is the pattern of mutating data through shared (immutable) references. Rust's Cell<T> and RefCell<T> enable this with runtime borrow checking, useful when compile-time borrow rules are too restrictive.

What is the performance cost of Arc vs Rc?

Arc uses atomic operations for reference counting, which are slightly slower than Rc's non-atomic operations. In practice, the overhead is negligible unless you clone Arc millions of times per second.

Can RefCell be used across threads?

No. RefCell is not Sync. For thread-safe interior mutability, use Mutex<T> or RwLock<T> inside an Arc.

Closures & Iterators
Concurrency
Unsafe Rust

What's Next

Explore Rust Concurrency for threads, channels, and mutex, and Unsafe Rust for raw pointer manipulation.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro