Rust / Advanced / Lesson 13
Smart pointers and interior mutability
Choose exclusive, shared, and dynamically checked access models explicitly, and keep cleanup and reference cycles separate from memory-safety claims.
On this page
Lesson goals
- Compare Box, Rc, Weak, Cell, and RefCell.
- Explain RefCell borrow guards and fallible borrow methods.
- Avoid shared-owner cycles and unnecessary shared ownership.
Choose the simplest lifetime model
A local owned value is often sufficient. Box<T> gives exclusive ownership of a heap allocation and can represent recursive structures or dynamically sized values behind a pointer. Rc<T> supports multiple owners within one thread through non-atomic reference counting. Weak observes without keeping the value alive and can break owning cycles. These handles solve ownership, not arbitrary mutation. If one function can borrow a value temporarily, reference counting may add complexity without benefit. Write the ownership graph separately from the access graph so a convenient back-reference does not accidentally become another owner.
A guard keeps a dynamic borrow active
borrow and borrow_mut can panic when the runtime borrowing rule is violated. try_borrow and try_borrow_mut report a recoverable failure instead. The second example deliberately keeps a shared guard alive, confirms that exclusive access is unavailable, then drops the guard before updating. No invalid memory access or panic is required to demonstrate the conflict. Holding a guard across a callback can cause surprising reentrant borrow failures, so keep guard scopes small. A runtime check is not permission for simultaneous conflicting references; it is the mechanism that prevents them from being created through this safe interface.
use std::cell::RefCell;
fn main() {
let value = RefCell::new(4u32);
let read = value.borrow();
println!("{}", value.try_borrow_mut().is_err());
drop(read);
*value.borrow_mut() = 9;
println!("{}", value.borrow());
}
true
9
The shared guard blocks mutation. Dropping it permits an exclusive update, after which a fresh read sees nine.
Memory safety does not promise no leaks
A cycle made entirely of Rc owners can keep every strong count above zero, leaking values even in safe Rust. Use Weak for a logically observing back-link, then call upgrade and handle None when the target has expired. Destruction runs when the owning structure actually releases a value, not when a programmer wishes a graph were unreachable. Drop supports cleanup but should avoid panicking during unwinding. Explicit mem::forget and process abort can also prevent ordinary destruction. Do not base an unsafe safety invariant on a promise that arbitrary safe clients always run a destructor.
Compare mutation models by their obligations
Cell supports certain value replacement patterns without handing out ordinary shared references to its interior; RefCell supports dynamic borrow guards. Mutex and other synchronized types address different cross-thread requirements in the next lesson. Immutable shared data often needs only Rc or Arc, without a cell or lock. Prefer restructuring ownership when it removes a whole category of dynamic checks. Test nested callbacks, expiration of weak observers, and cleanup counts with synthetic resources. A program can remain memory-safe while panicking, leaking resources, or implementing the wrong update order, so test those product outcomes separately.
Common errors and fixes
RefCell panics during a nested callback.
- Why it happens
- A conflicting guard remains active across reentry.
- How to fix it
- Drop guards before callbacks or redesign the update sequence.
- Prevent it next time
- Keep dynamic borrow scopes short.
Cloned Rc handles unexpectedly share changes.
- Why it happens
- Cloning a handle did not clone the underlying value.
- How to fix it
- Create a separate owned value for a true snapshot.
- Prevent it next time
- State whether an API shares identity or copies data.
An Rc graph never releases resources.
- Why it happens
- Strong ownership links form a cycle.
- How to fix it
- Use an appropriate Weak back-link and check upgrade.
- Prevent it next time
- Review ownership cycles independently of reachability.
Check your understanding
1. Replace Rc::clone in the first example with an independently cloned vector in a new RefCell. Explain how the observed length through original changes.
Show worked solution
The new handle would own a different vector initialized with the same single element. Appending through that independent owner leaves original at length one, rather than two. This is a change from shared identity to snapshot semantics. Borrow the original briefly to clone its vector contents, then end that guard before later mutation or callbacks.
2. Two nodes own each other with Rc and external handles are dropped. Explain the repair and how callers should observe the repaired back-link.
Show worked solution
Replace the logically non-owning back-link with Weak. Removing external owners can then let one node drop and release its owning edge to the other. Observers call upgrade and handle None instead of assuming the target still exists. The chosen weak direction must follow the domain ownership model rather than being selected only to make a test pass.
Key takeaways
- Separate shared ownership from mutation policy.
- Dynamic borrow guards still enforce exclusive access.
- Review cycles, reentry, and cleanup independently.