Chapter 30: Smart Pointers and Interior Mutability
Prerequisites
You will understand
Box,Rc,Arc— different ownership counts- Interior mutability: rule relocation, not removal
- Why
Rc<RefCell<T>>is sometimes a code smell
Reading time
Box for heap, Rc/Arc for shared ownership, RefCell/Mutex for interior mutability.Revisit Ch 20 →Arc<Mutex<T>> is the standard pattern for shared mutable state across threads. Ch 32 shows when to use it vs message passing.Ch 32: Shared State →Different Pointers Encode Different Meanings
The Borrow Rule Still Exists, but Enforcement Moves
Step 1 - The Problem
Ownership and borrowing cover most programs, but not all ownership shapes are “one owner, straightforward borrows.”
Sometimes you need:
- heap allocation independent of stack size
- multiple owners
- mutation behind shared references
- shared mutable state across threads
The temptation is to treat smart pointers as “ways to satisfy the borrow checker.” That is exactly the wrong mental model.
Step 2 - Rust’s Design Decision
Rust offers different smart pointers because they represent different invariants:
Box<T>for owned heap allocationRc<T>for shared ownership in single-threaded codeArc<T>for shared ownership across threadsCell<T>andRefCell<T>for single-threaded interior mutabilityMutex<T>andRwLock<T>for thread-safe interior mutability
Rust accepted:
- more pointer types
- explicit runtime-cost choices
Rust refused:
- one universal reference-counted mutable object model
- hidden shared mutability everywhere
Step 3 - The Mental Model
Plain English rule: choose the pointer for the ownership shape you mean.
Ask two questions:
- how many owners are there?
- where is mutation allowed and who synchronizes it?
Step 4 - Minimal Code Example
#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::rc::Rc;
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
shared.borrow_mut().push(4);
assert_eq!(shared.borrow().len(), 4);
}
Step 5 - Line-by-Line Compiler Walkthrough
Rc::new(...)creates shared ownership with non-atomic reference counting.RefCell::new(...)allows mutation checked at runtime instead of compile time.borrow_mut()returns a runtime-checked mutable borrow guard.- If another borrow incompatible with that mutable borrow existed simultaneously,
RefCellwould panic.
The invariant here is not “mutability is free now.” It is:
the aliasing rule still exists, but enforcement moved from compile time to runtime.
Step 6 - Three-Level Explanation
Smart pointers are not just pointers. Each one adds a rule about ownership or mutation.
Pick them deliberately:
Box<T>when you need heap storage, recursive types, or trait objectsRc<T>when many parts of one thread need shared ownershipArc<T>when many threads need shared ownershipRefCell<T>when a single-threaded design truly needs interior mutabilityMutex<T>orRwLock<T>when cross-thread mutation must be synchronized
Each smart pointer trades one cost for another:
Box<T>: allocation, but simple semanticsRc<T>: refcount overhead, not thread-safeArc<T>: atomic refcount overhead, thread-safeRefCell<T>: runtime borrow checks, panic on violationMutex<T>: locking cost and deadlock risk
These are design decisions, not borrow-checker escape hatches.
Box<T>, Trait Objects, and Recursive Types
Box<T> matters because some types need indirection:
- recursive enums
- heap storage separate from stack frame size
- trait objects like
Box<dyn Error>
It is the simplest smart pointer: single owner, no shared state semantics.
Rc<T> vs Arc<T>
The distinction is not “local versus global.” It is atomicity:
Rc<T>is cheaper, but not thread-safeArc<T>is safe across threads, but pays atomic refcount costs
If you are not crossing threads, Rc<T> is usually the better fit.
Interior Mutability
Interior mutability exists because sometimes &self methods must still update hidden state:
- memoization
- cached parsing
- mock recording in tests
- counters or deferred initialization
Single-threaded:
Cell<T>for smallCopydataRefCell<T>for richer borrowed access patterns
Multi-threaded:
Mutex<T>RwLock<T>
The important design question is always:
why is shared outer access compatible with hidden inner mutation here?
Avoiding Rc<RefCell<T>> Hell
Rc<RefCell<T>> is sometimes the right tool. It is also one of the clearest smells in beginner Rust when used everywhere.
Why it goes wrong:
- ownership boundaries disappear
- runtime borrow panics replace compile-time reasoning
- graph-like object models from other languages get imported without redesign
Alternatives often include:
- clearer single ownership plus message passing
- indices into arenas
- staged mutation
- redesigning APIs so borrowing is local instead of global
Step 7 - Common Misconceptions
Wrong model 1: “Smart pointers are for making the borrow checker happy.”
Correction: they encode real ownership and mutation semantics.
Wrong model 2: “Rc<RefCell<T>> is idiomatic anytime ownership is hard.”
Correction: sometimes necessary, often a sign the design needs reshaping.
Wrong model 3: “Arc is just the thread-safe Box.”
Correction: it is shared ownership with atomic refcounting, not mere heap allocation.
Wrong model 4: “Interior mutability breaks Rust’s rules.”
Correction: it keeps the rules but enforces some of them at runtime or under synchronization.
Step 8 - Real-World Pattern
You see:
Box<dyn Error>and boxed trait objects at abstraction boundariesArc-wrapped shared app state in servicesMutexandRwLockaround caches and registriesRefCellin tests, single-threaded caches, and some compiler-style interior bookkeeping
Strong code treats these as deliberate boundary tools rather than default building blocks.
Step 9 - Practice Block
Code Exercise
For each scenario, pick a pointer and justify it:
- recursive AST node
- shared cache in one thread
- shared config across worker threads
- mutable test double used through
&self
Code Reading Drill
What two independent meanings are encoded here?
#![allow(unused)]
fn main() {
let state = Arc::new(Mutex::new(HashMap::<String, usize>::new()));
}
Spot the Bug
Why is this suspicious design?
#![allow(unused)]
fn main() {
struct App {
state: Rc<RefCell<HashMap<String, String>>>,
}
}
Assume this sits at the heart of a growing application.
Refactoring Drill
Take a design relying on Rc<RefCell<T>> across many modules and redesign it with one clear owner plus borrowed views or messages.
Compiler Error Interpretation
If the compiler says Rc<T> cannot be sent between threads safely, translate that as: “this ownership-sharing tool was designed only for single-threaded use.”
Step 10 - Contribution Connection
After this chapter, you can read and improve:
- app-state wiring
- cache internals
- trait-object boundaries
- shared ownership and mutation decisions
Good first PRs include:
- replacing unnecessary
Arc<Mutex<_>>layers - documenting why a smart pointer is used
- simplifying designs that overuse
Rc<RefCell<T>>
In Plain English
Smart pointers exist because not all ownership problems look the same. Some values need heap storage, some need many owners, and some need carefully controlled hidden mutation. Rust makes those differences explicit so you pay only for the behavior you actually need.
What Invariant Is Rust Protecting Here?
Pointer-like abstractions must preserve the intended ownership count, mutation discipline, and thread-safety guarantees rather than collapsing all sharing into one vague mutable object model.
If You Remember Only 3 Things
- Pick smart pointers for ownership shape, not as a reflex.
- Interior mutability moves enforcement, but it does not erase the aliasing rule.
Rc<RefCell<T>>can be valid, but widespread use often signals missing structure.
Memory Hook
Smart pointers are different kinds of building keys. Box is one key. Rc is many copies of one key for one building. Arc is many secured badges for a cross-site campus. RefCell and Mutex are the locked cabinets inside.
Flashcard Deck
| Question | Answer |
|---|---|
What is Box<T> mainly for? | Single-owner heap allocation, recursive types, and trait-object storage. |
What is the key difference between Rc<T> and Arc<T>? | Arc uses atomic reference counting for thread safety; Rc does not. |
What does RefCell<T> do? | Provides interior mutability with runtime borrow checking in single-threaded code. |
What is Cell<T> best for? | Small Copy values that need simple interior mutation. |
What does Mutex<T> add? | Thread-safe exclusive access via locking. |
| Does interior mutability remove Rust’s aliasing rule? | No. It changes how and when the rule is enforced. |
Why can Rc<RefCell<T>> become a smell? | It often hides poor ownership design and replaces compile-time reasoning with runtime panics. |
| What question should guide smart-pointer choice? | How many owners exist, and how is mutation synchronized or restricted? |
Chapter Cheat Sheet
| Need | Prefer | Why |
|---|---|---|
| One owner on heap | Box<T> | simple indirection |
| Shared ownership in one thread | Rc<T> | cheap refcount |
| Shared ownership across threads | Arc<T> | atomic refcount |
| Hidden mutation in one thread | Cell<T> / RefCell<T> | interior mutability |
| Hidden mutation across threads | Mutex<T> / RwLock<T> | synchronized access |