Box, Rc, Arc, and RefCell, heap allocation, reference counting, and interior mutability.
Box<T> is the simplest smart pointer. It allocates data on the heap and stores a pointer on the stack. Use it when you need a value with a known size at compile time to live on the heap, or when you have a recursive type whose size can't be known at compile time.
// Basic Box: allocate on heap
let b = Box::new(5);
println!("b = {}", b);
// Boxing values into a Vec
let mut list: Vec<Box<i32>> = vec![
Box::new(1),
Box::new(2),
Box::new(3),
];
// Recursive type with Box
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:?}", list);
Box<T> implements Deref, so a &Box<String> automatically coerces to &str. This means you can pass a boxed value anywhere a reference is expected.
// Box with trait objects for dynamic dispatch
trait Animal {
fn speak(&self) -> String;
struct Dog;
impl Animal for Dog {
fn speak(&self) -> String {
String::from("Woof!")
}
struct Cat;
impl Animal for Cat {
fn speak(&self) -> String {
String::from("Meow!")
}
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for animal in animals {
println!("{}", animal.speak());
}
// Box size: only a pointer on the stack
struct Large {
data: [u8; 1024],
// Without Box: 1 KB on the stack
// With Box: only 8 bytes (pointer) on the stack
let large = Box::new(Large { data: [0; 1024] });
// Deref to get the inner value
let b = Box::new(5);
println!("{}", *b); // dereference
let b = Box::new(String::from("hello"));
println!("{}", &b[0..3]); // auto-deref: "hel"
Rc<T> (Reference Counted) enables multiple ownership of the same heap data in single-threaded scenarios. Each call to Rc::clone increments a reference count; when the last Rc is dropped, the data is freed.
use std::rc::Rc;
// Basic Rc: multiple ownership
let a = Rc::new(5);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("a: {}, b: {}, c: {}", a, b, c);
println!("Reference count: {}", Rc::strong_count(&a)); // 3
use std::rc::Rc;
// Reference counting demo
let data = Rc::new(String::from("shared data"));
let owner1 = Rc::clone(&data);
let owner2 = Rc::clone(&data);
println!("Count: {}", Rc::strong_count(&data)); // 3
drop(owner1);
println!("Count after drop: {}", Rc::strong_count(&data)); // 2
use std::rc::Rc;
// Tracking the reference count through scopes
let rc_vec = Rc::new(vec![1, 2, 3]);
println!("Initial count: {}", Rc::strong_count(&rc_vec)); // 1
{
let _temp = Rc::clone(&rc_vec);
println!("Inner count: {}", Rc::strong_count(&rc_vec)); // 2
println!("After scope: {}", Rc::strong_count(&rc_vec)); // 1
Rc<T> does not implement Send. It uses non-atomic reference counting and must only be used in single-threaded code. For multi-threaded scenarios, use Arc<T> instead.
Arc<T> (Atomically Reference Counted) is the thread-safe counterpart to Rc<T>. It uses atomic operations for its reference count, making it safe to share across threads at a small performance cost.
use std::sync::Arc;
use std::thread;
// Basic Arc: atomic reference counting
let data = Arc::new(vec![1, 2, 3]);
let data2 = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("Thread sees: {:?}", *data2);
});
println!("Main: {:?}", *data);
handle.join().unwrap();
use std::sync::{Arc, Mutex};
use std::thread;
// Multiple threads sharing a counter
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
for handle in handles {
handle.join().unwrap();
println!("Result: {}", *counter.lock().unwrap()); // 10
use std::sync::{Arc, Weak};
// Weak references in Arc (prevent cycles)
let strong = Arc::new(42);
let weak = Arc::downgrade(&strong);
match weak.upgrade() {
Some(strong_ref) => println!("Value: {}", strong_ref),
None => println!("Value was dropped"),
}
Rc is faster because it uses non-atomic operations. Arc uses atomic operations for thread safety at a slight performance cost. Use Rc in single-threaded code, Arc when sharing across threads.
RefCell<T> enforces Rust's borrowing rules at runtime instead of compile time. This enables interior mutability, mutating data even when there are immutable references to it. Violating the rules causes a panic instead of a compile error.
use std::cell::RefCell;
// Interior mutability: mutate behind an immutable reference
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow for reading
{
let borrowed = data.borrow();
println!("Data: {:?}", *borrowed);
} // borrow ends here
// Mutable borrow for modifying
{
let mut borrowed_mut = data.borrow_mut();
borrowed_mut.push(4);
} // mutable borrow ends here
println!("After mutation: {:?}", *data.borrow());
use std::cell::RefCell;
// RefCell with structs
struct Person {
name: String,
age: RefCell<u32>,
let person = Person {
name: String::from("Alice"),
age: RefCell::new(30),
};
println!("Age: {}", person.age.borrow());
*person.age.borrow_mut() += 1;
println!("New age: {}", person.age.borrow());
RefCell panics if you try to borrow mutably while an immutable borrow is active (or vice versa). Always drop borrows before creating new ones. Use try_borrow() and try_borrow_mut() for safe, non-panicking alternatives.
use std::cell::RefCell;
// Mock object pattern with RefCell
trait Logger {
fn log(&self, msg: &str);
struct MockLogger {
messages: RefCell<Vec<String>>,
impl Logger for MockLogger {
fn log(&self, msg: &str) {
// Mutate through &self thanks to RefCell
self.messages.borrow_mut().push(msg.to_string());
}
let logger = MockLogger {
messages: RefCell::new(Vec::new()),
};
logger.log("Event 1");
logger.log("Event 2");
println!("Logs: {:?}", logger.messages.borrow());
Weak<T> is a non-owning reference that doesn't prevent the data from being dropped. It breaks reference cycles that would otherwise cause memory leaks when using Rc or Arc.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
let parent = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
// Child points back to parent with Weak (doesn't prevent cleanup)
*child.parent.borrow_mut() = Rc::downgrade(&parent);
parent.children.borrow_mut().push(Rc::clone(&child));
println!("Parent strong count: {}", Rc::strong_count(&parent)); // 1
println!("Child strong count: {}", Rc::strong_count(&child)); // 2
use std::rc::Rc;
// Downgrade and upgrade pattern
let strong = Rc::new(42);
let weak = Rc::downgrade(&strong);
println!("Strong count: {}", Rc::strong_count(&strong)); // 1
println!("Weak count: {}", Rc::weak_count(&strong)); // 1
// strong exists, upgrade succeeds
if let Some(value) = weak.upgrade() {
println!("Upgraded successfully: {}", value);
drop(strong);
println!("After drop: {:?}", weak.upgrade()); // None
Use Weak for parent-child relationships in trees (children own strong refs; parent back-pointers use Weak), observer patterns where observers can be dropped, and cache entries that should be evictable.
The Deref trait lets you customise the * dereference operator, enabling smart pointers to behave like regular references. The Drop trait runs cleanup code when a value goes out of scope.
use std::ops::Deref;
// Custom smart pointer with Deref
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
let b = MyBox(5);
println!("Value: {}", *b); // Uses Deref
// Deref coercion: MyBox<String> -> &String -> &str
fn print_length(s: &str) {
println!("Length: {}", s.len());
let my_string = MyBox(String::from("hello"));
print_length(&my_string); // Deref coercion chain
// Drop trait: cleanup when a value goes out of scope
struct Resource {
id: u32,
impl Drop for Resource {
fn drop(&mut self) {
println!("Releasing resource {}", self.id);
}
{
let r1 = Resource { id: 1 };
let r2 = Resource { id: 2 };
println!("Resources created");
// Output:
// Resources created
// Releasing resource 2 (dropped in reverse order)
// Releasing resource 1
// Manual early drop with std::mem::drop
let resource = Resource { id: 5 };
println!("Created resource");
drop(resource); // Explicitly drop early
println!("Resource dropped");
Values are dropped in reverse order of creation. You cannot call .drop() directly, use std::mem::drop(value) to force an early drop.
Choosing the right smart pointer depends on your ownership and threading requirements. Here is a concise decision guide.
| Scenario | Smart Pointer |
|---|---|
| Single owner, heap allocation | Box<T> |
| Multiple owners (single-thread) | Rc<T> |
| Multiple owners (multi-thread) | Arc<T> |
Mutate through & | RefCell<T> |
| Thread-safe mutation | Mutex<T> / RwLock<T> |
| Shared + mutable (single-thread) | Rc<RefCell<T>> |
| Shared + mutable (multi-thread) | Arc<Mutex<T>> |
| Prevent reference cycles | Weak<T> |
use std::rc::Rc;
use std::cell::RefCell;
// Pattern 1: Rc + RefCell, shared mutable state (single-threaded)
let data = Rc::new(RefCell::new(42));
let d2 = Rc::clone(&data);
*d2.borrow_mut() = 100;
println!("{}", data.borrow()); // 100
use std::sync::{Arc, Mutex};
use std::thread;
// Pattern 2: Arc + Mutex, shared mutable state (multi-threaded)
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || {
*c.lock().unwrap() += 1;
});
Building a linked list in Rust is a classic exercise that combines Rc, RefCell, and Option. This example demonstrates shared ownership, interior mutability, and recursive data structures.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct LinkedListNode {
value: i32,
next: Option<Rc<RefCell<LinkedListNode>>>,
impl LinkedListNode {
fn new(value: i32) -> Self {
LinkedListNode { value, next: None }
}
#[derive(Debug)]
struct LinkedList {
head: Option<Rc<RefCell<LinkedListNode>>>,
length: usize,
impl LinkedList {
fn new() -> Self {
LinkedList { head: None, length: 0 }
}
fn push_front(&mut self, value: i32) {
let mut new_node = LinkedListNode::new(value);
new_node.next = self.head.take();
self.head = Some(Rc::new(RefCell::new(new_node)));
self.length += 1;
}
fn pop(&mut self) -> Option<i32> {
self.head.take().map(|head| {
self.head = head.borrow_mut().next.take();
self.length -= 1;
head.borrow().value
})
}
}
fn main() {
let mut list = LinkedList::new();
list.push_front(1);
list.push_front(2);
list.push_front(3);
// List: 3 -> 2 -> 1 -> None
println!("Length: {}", list.length); // 3
println!("Popped: {:?}", list.pop()); // Some(3)
// List: 2 -> 1 -> None
}
Real-world Rust rarely needs hand-rolled linked lists. The standard library provides Vec, VecDeque, and LinkedList. However, building one yourself is the best way to internalise how Rc, RefCell, and Option compose together.