Rust Smart Pointers â Box, Rc, Arc & RefCell
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
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
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
Related Concepts
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