Understanding Box, Rc, and Arc in Rust: A Practical Guide

When you're learning Rust, one of the most important concepts to grasp is memory management through smart pointers. Unlike languages with garbage collection, Rust gives you precise control over where your data lives and who owns it. Three fundamental smart pointers you'll encounter are Box<T>
, Rc<T>
, and Arc<T>
.
In this post, I'll walk through each of these smart pointers with practical examples from my codebase, explaining when and why you'd use each one.
Box: Single Ownership with Heap Allocation
Box<T>
is the simplest smart pointer. It allocates data on the heap while maintaining single ownership semantics. Think of it as a way to store large data structures without overflowing your stack.
Here's a practical example with a large data structure:
#[derive(Debug)]
struct BigData {
data : [u8;1024] // 1 KB fixed array
}
fn main() {
let var1 = BigData {
data : [1;1024]
};
let var2 = var1; // ownership transferred
println!("var2 took ownership: {:?}", var2);
// Box allocates on heap
let var3 = Box::new(var2);
println!("Heap pointer address: {:p}", var3);
}
The key insight here is that var3
only stores a pointer on the stack, while the actual BigData
lives on the heap. This is crucial when dealing with large structures that might cause stack overflow if stored directly.
When to use Box:
Large data structures that shouldn't live on the stack
Recursive data structures like linked lists or trees
When you need a stable memory address
Dynamic dispatch with trait objects
Rc: Reference Counting for Single-Threaded Sharing
Rc<T>
(Reference Counted) allows multiple owners of the same data within a single thread. It keeps track of how many references exist and deallocates the data when the count reaches zero.
Here's how it works in practice:
use std::{cell::RefCell, rc::Rc};
fn main() {
let var_vec = vec![1,2,3,4];
// Create the first Rc owner
let rc_var_vec = Rc::new(var_vec);
println!("Initial data: {:?}", rc_var_vec);
// Check the memory address
let ptr = rc_var_vec.as_ptr();
println!("Memory address: {:?}", ptr);
// Check reference count
let counter_cur = Rc::strong_count(&rc_var_vec);
println!("Reference count: {}", counter_cur); // Should be 1
// Create additional owners
let rc_var_vec2 = Rc::clone(&rc_var_vec);
let rc_var_vec3 = Rc::clone(&rc_var_vec2);
println!("Data from third reference: {:?}", rc_var_vec3);
println!("Reference count now: {}", Rc::strong_count(&rc_var_vec3)); // Should be 3
// All references point to the same memory location
println!("Same memory address: {:?}", rc_var_vec2.as_ptr());
}
Notice that Rc::clone()
doesn't clone the data itself, just increments the reference counter and gives you another pointer to the same data.
Adding Mutability with RefCell
By default, Rc<T>
only provides immutable access because multiple owners exist. To enable mutation, we combine it with RefCell<T>
:
fn demonstrate_rc_mutability() {
let mutable_vec = vec![1,2,3,4];
let rc_mutable_vec = Rc::new(RefCell::new(mutable_vec));
// Immutable borrow
let borrowed_data = rc_mutable_vec.borrow();
println!("Borrowed data: {:?}", borrowed_data);
drop(borrowed_data); // Must drop before mutable borrow
// Mutable borrow
rc_mutable_vec.borrow_mut().extend([5,6]);
println!("After mutation: {:?}", rc_mutable_vec.borrow());
}
When to use Rc:
- Single-threaded programs that need shared ownership
Arc: Thread-Safe Reference Counting
Arc<T>
(Atomic Reference Counted) is the thread-safe version of Rc<T>
. It uses atomic operations to manage the reference count, making it safe to share across multiple threads.
Here's a practical threading example:
use std::{sync::Arc, thread, time::Duration};
fn main() {
let sample_data = vec![1, 12];
// Wrap data in Arc for thread-safe sharing
let arc_sample_data = Arc::new(sample_data);
println!("Initial reference count: {}", Arc::strong_count(&arc_sample_data));
let mut handles = vec![];
// Spawn 3 threads, each gets a clone of the Arc
for i in 1..4 {
let arc_cloned_var = Arc::clone(&arc_sample_data);
let handle = thread::spawn(move || {
// Manually dereference for clarity
println!("Thread {} processing data: {:?}", i, *arc_cloned_var);
thread::sleep(Duration::from_secs(1));
println!("Thread {} (ID: {:?}) finished", i, thread::current().id());
});
handles.push(handle);
}
// Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
println!("All threads completed");
}
The beauty of Arc
is that each thread gets its own reference to the same data, and the atomic reference counting ensures memory safety without requiring locks for the basic sharing mechanism.
When to use Arc:
Sharing immutable data across multiple threads
Thread pools that need access to shared configuration
Concurrent data processing where multiple threads read the same dataset
Memory Layout Understanding
Understanding how these smart pointers work internally helps you make better decisions:
Stack vs Heap Layout:
Stack: Heap:
[Box ptr] -> [data]
[Rc ptr] -> [ref_count | data]
[Arc ptr] -> [atomic_ref_count | data]
The stack only holds a pointer, while the heap contains both metadata (reference counts) and the actual data.
Conclusion
Smart pointers in Rust give you fine-grained control over memory management while maintaining safety guarantees. Box
handles single ownership with heap allocation, Rc
enables shared ownership in single-threaded contexts, and Arc
extends that sharing to multi-threaded environments.
Choosing the Right Smart Pointer
Here's a decision tree for choosing between these smart pointers:
Single ownership, heap allocation needed: Use
Box<T>
Multiple ownership, single-threaded: Use
Rc<T>
Multiple ownership, multi-threaded: Use
Arc<T>
Need mutation with shared ownership: Combine with
RefCell<T>
(single-threaded) orMutex<T>
(multi-threaded)
The key is understanding when you need single vs multiple ownership, and whether you're working in a single-threaded or multi-threaded context. Start with Box
for simple heap allocation, move to Rc
when you need sharing within a thread, and reach for Arc
when threads are involved.
In the next blog post, we'll dive deeper into Mutex
and Semaphore
for thread-safe mutable access patterns, exploring how they work with Arc
to enable safe concurrent programming in Rust.
Repository: All code examples are available at github.com/Ashwin-3cS/box-arc-rc-mutex-semaphore
Subscribe to my newsletter
Read articles from Ashwin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ashwin
Ashwin
I'm a Full Stack Web3 Engineer crafting cutting-edge dApps and DeFi solutions. From writing secure smart contracts to building intuitive Web3 interfaces, I turn complex blockchain concepts into user-friendly experiences. I specialize in building on Ethereum, Sui, and Aptos — blockchain platforms where I’ve developed and deployed production-grade, battle-tested smart contracts. My experience includes working with both Solidity on EVM chains and Move on Sui and Aptos. I'm passionate about decentralization, protocol development, and shaping the infrastructure for Web3's future.