Smart Pointers

Course 2 · Ch 4
Smart Pointers
Relocating, not abandoning, Course 1's ownership rules for the cases that need more flexibility

Course 1's ownership rules — exactly one owner, borrow checking enforced entirely at compile time — are strict by design. Some real, legitimate patterns genuinely don't fit that shape. Smart pointers are Rust's controlled, still-safe way to get flexibility where it's actually needed, without abandoning safety altogether.

Box<T>: Heap Allocation With a Single Owner

The simplest smart pointer: puts a value on the heap instead of the stack, while still following ordinary single-ownership rules exactly — Box<T> itself is the single owner, moving per Course 1 Chapter 3's rules like anything else. The classic use case is a recursive type:

enum List { Cons(i32, Box<List>), Nil, }

Without Box, Cons(i32, List) fails to compile — the compiler needs to know a fixed size for List at compile time, but a List containing a List containing a List... has no fixed size at all. Box's size is always just one pointer, regardless of what it points to, breaking the infinite recursion.

Rc<T>: Multiple Owners via Reference Counting

Course 1's rule was exactly one owner. Rc<T> ("Reference Counted") relaxes this specifically for cases where multiple parts of a program genuinely need to jointly own the same data:

use std::rc::Rc; let a = Rc::new(5); let b = Rc::clone(&a); // increments the count — no deep copy println!("count: {}", Rc::strong_count(&a)); // 2

Rc::clone doesn't copy the underlying data — it increments a reference count. The data is only actually dropped once the last Rc pointing to it goes out of scope, bringing the count to zero.

RefCell<T> and Interior Mutability

The borrow checker's rules (Course 1, Chapter 4) are enforced at compile time by default. RefCell<T> moves that same rule — one mutable OR many immutable borrows — to runtime instead, allowing mutation even through an immutable reference to the RefCell itself:

use std::cell::RefCell; let value = RefCell::new(5); *value.borrow_mut() += 1; println!("{}", value.borrow()); // 6 // Violating the rule at runtime — this PANICS instead of failing to compile: // let _b1 = value.borrow_mut(); // let _b2 = value.borrow_mut(); // thread panicked: already borrowed

A genuine trade-off: more flexibility, but a violated rule moves from a compile error to a runtime panic.

Combining Rc<T> and RefCell<T>

The common, idiomatic combination — Rc<RefCell<T>> — gives multiple owners that can each mutate the shared data. This is Rust's deliberate, opt-in answer to a pattern that's simply the default in a garbage-collected language (shared mutable state) — not worse, just something you have to explicitly ask for rather than get for free:

let shared = Rc::new(RefCell::new(0)); let owner_a = Rc::clone(&shared); let owner_b = Rc::clone(&shared); *owner_a.borrow_mut() += 10; *owner_b.borrow_mut() += 5; println!("{}", shared.borrow()); // 15 — both owners mutated the SAME underlying data
Compile-Time Borrow Checking (Ch.4, Course 1)RefCell<T> (Runtime)
When rules are checkedAt compile timeAt runtime, on each borrow()/borrow_mut() call
Violating the ruleCompile errorPanic
FlexibilityLess — some valid patterns rejectedMore — patterns the compiler can't verify statically still work

Box<T>

Single ownership, heap-allocated — no rule relaxation, just placement. Needed for recursive types.

Rc<T>

Relaxes single ownership via reference counting — multiple joint owners of the same data.

RefCell<T>

Relaxes compile-time borrow checking to runtime — mutation through an immutable reference.

Rc<RefCell<T>>

Both relaxations combined — multiple owners, each able to mutate the shared data.

Not Abandoning the Rules — Relocating Them
Course 1 established that ownership and borrowing are checked strictly, at compile time. Smart pointers don't throw that away: Box changes nothing about the rules, only where data lives; Rc relaxes single-ownership via counting; RefCell relaxes borrow-checking from compile time to runtime. Every one of them is a deliberate, scoped exception — not a general escape hatch from Rust's safety model.
Rc<T> Alone Does Not Allow Mutation
A common early mix-up: assuming Rc<T> by itself is "the mutable shared pointer." It isn't — Rc<T>'s data is immutable by default, which is exactly why it's so often paired with RefCell<T> specifically for the mutable case. Also worth flagging ahead: Rc<T> is not thread-safe — Arc<T> (the atomic version) is what Chapter 5's concurrency material uses instead.

Coding Challenges

Challenge 1

Explain why enum Tree { Leaf(i32), Node(Box, Box) } compiles, but enum Tree { Leaf(i32), Node(Tree, Tree) } (without Box) does not — referencing this chapter's List example.

📄 View solution
Challenge 2

Write code creating an Rc, cloning it twice (three total owners), printing the strong count after each clone, then dropping one clone explicitly with drop() and printing the count once more.

📄 View solution
Challenge 3

Using Rc>>, write code where two separate "owner" variables each push a different number onto the same shared vector, then print the vector's final contents from a third owner to prove all three see the same underlying data.

📄 View solution

Chapter 4 Quick Reference

  • Box<T> — single-owner heap allocation; needed for recursive types (fixed-size pointer breaks infinite size)
  • Rc<T> — multiple owners via reference counting; Rc::clone increments the count, no deep copy
  • Rc::strong_count(&x) — inspect how many owners currently exist
  • RefCell<T> — moves borrow checking to runtime; violating the rule panics instead of failing to compile
  • Rc<RefCell<T>> — the idiomatic combo: multiple owners, each able to mutate shared data
  • Rc<T> alone is immutable — RefCell is what actually enables mutation through it
  • Rc<T> is not thread-safe — Arc<T> is the concurrency-safe equivalent, coming next chapter
  • Next chapter: Concurrency — threads, Arc<Mutex<T>>, mpsc channels, and "fearless concurrency" contrasted with Go's goroutines