PHASE 3 ← Back to Course
10 / 17
🧠

Smart Pointers

Box, Rc, Arc, and RefCell, heap allocation, reference counting, and interior mutability.

1

Box<T> for Heap Allocation

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.

Rust
// 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),
];
Rust
// 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);
💡

Deref Coercion

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.

Rust
// 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());
}
Rust
// 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"
2

Rc<T> for Reference Counting

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.

Rust
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
Rust
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
Rust
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 is NOT Thread-Safe

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.

3

Arc<T> for Thread-Safe Reference Counting

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.

Rust
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();
Rust
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
Rust
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"),
}
💡

Arc vs Rc

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.

4

RefCell<T> and Interior Mutability

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.

Rust
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());
Rust
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());
⚠️

Runtime Panics

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.

Rust
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());
5

Weak<T> to Prevent Cycles

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.

Rust
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
Rust
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

When to Use Weak

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.

6

Deref and Drop Traits

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.

Rust
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
Rust
// 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");
💡

Drop Order

Values are dropped in reverse order of creation. You cannot call .drop() directly, use std::mem::drop(value) to force an early drop.

7

When to Use Which Smart Pointer

Choosing the right smart pointer depends on your ownership and threading requirements. Here is a concise decision guide.

ScenarioSmart Pointer
Single owner, heap allocationBox<T>
Multiple owners (single-thread)Rc<T>
Multiple owners (multi-thread)Arc<T>
Mutate through &RefCell<T>
Thread-safe mutationMutex<T> / RwLock<T>
Shared + mutable (single-thread)Rc<RefCell<T>>
Shared + mutable (multi-thread)Arc<Mutex<T>>
Prevent reference cyclesWeak<T>
Rust
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
Rust
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;
});
8

Putting It Together: A Linked List

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.

Rust
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
        })
    }
}
Rust
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
}

Smart Pointers in Practice

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.

← Previous Chapter 10 of 17 Next →