21.xChapter 21 summary and quiz

Last updated June 13, 2026

This chapter named a pattern you'd been using all along and added three deliberate tools to it. Review, then a capstone that combines them.

Quick review

A smart pointer is a value that owns data and adds behavior, you've used String and Vec as smart pointers since chapter 1 (21.1). Two traits make one: Deref (reach through it like a pointer) and Drop (clean up when it dies).

Box<T> puts a single value on the heap with one owner (21.2). Its essential use is recursive types: enum List { Cons(i32, Box<List>), Nil } compiles where the un-boxed version has "infinite size," because a Box is a fixed-size pointer. Also used for trait objects and large moves.

Deref defines * for a type, returning a reference to its inner value (21.3). Deref coercion auto-inserts deref calls so &Box<String> works where &str is wanted, which is exactly why &String works as &str (the chapter-9 mystery, solved).

Drop runs cleanup code when a value goes out of scope (21.4); values drop in reverse declaration order; drop early with std::mem::drop(v) (never v.drop()). This is RAII: resource lifetime tied to value lifetime, the reason Rust needs neither manual frees nor a garbage collector.

Rc<T> gives shared ownership by reference counting (21.5): Rc::clone bumps a count cheaply (no deep copy), and the value frees when the last owner drops. Shared but immutable, and single-threaded only (Arc for threads, chapter 22).

RefCell<T> moves the borrow check to run time, enabling interior mutability, mutation through a shared reference via borrow()/borrow_mut(), panicking (BorrowMutError) on violations instead of failing to compile (21.6). Rc<RefCell<T>> combines shared ownership with shared mutability.

Reference cycles of strong Rcs leak memory (a safe bug, wasteful but not corrupting); Weak<T> breaks them with non-owning references read via upgrade() (21.7).

Quiz time

Question #1

Why must a recursive type like a cons list put its recursive field behind a Box (or Rc)?

Show solution

Without indirection, computing the type's size requires knowing its own size (it contains itself), giving "infinite size" (E0072), so the compiler can't lay it out on the stack. A Box (or Rc) is a fixed-size pointer regardless of what it points to, so wrapping the recursive field makes the type a known, finite size. The compiler's error message even suggests inserting a Box, Rc, or &.

Question #2

Rc<T> and RefCell<T> each give up something that &/&mut provide. What, and why combine them?

Show solution

Rc<T> allows multiple owners but only shared (immutable) access. RefCell<T> allows mutation through a shared reference but only single ownership, and it checks the borrow rules at runtime (panicking on violation) rather than compile time. Combining them as Rc<RefCell<T>> gives data that is both shared (many owners, via Rc) and mutable (via RefCell), which neither provides alone.

Question #3

Predict the output:

struct Loud(i32);
impl Drop for Loud {
    fn drop(&mut self) { println!("drop {}", self.0); }
}
fn main() {
    let a = Loud(1);
    let b = Loud(2);
    drop(a);
    println!("end of main");
}
Show solution
drop 1
end of main
drop 2

drop(a) (the std::mem::drop function) ends a's life immediately, printing drop 1. Then end of main prints. At the closing brace, b is dropped (a already is), printing drop 2. Note b drops at scope end in reverse-declaration order, but a was dropped early by hand, so only b remains for the automatic cleanup.

Question #4

A tree node holds Rc pointers to its children and you want each child to also reference its parent. Why should the parent-pointer be a Weak, not an Rc?

Show solution

If both directions were strong Rcs, parent and child would each keep the other's count above zero, a reference cycle that leaks: neither is ever freed. Making the child-to-parent link a Weak means it doesn't contribute to the strong count, so it doesn't keep the parent alive, breaking the cycle. Ownership flows downward (parent owns children, strong); the upward back-reference is non-owning (weak), read safely via upgrade().

Chapter capstone

Build a small shared, mutable counter that several handles can increment, then print the final value. This exercises Rc (shared ownership) and RefCell (interior mutability) together, the combination you'll meet constantly.

Write a program that creates a counter starting at 0, makes three handles to it, increments through each handle a different number of times (1, 2, and 3), and prints the total.

Show solution
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let counter = Rc::new(RefCell::new(0));

    let h1 = Rc::clone(&counter);
    let h2 = Rc::clone(&counter);
    let h3 = Rc::clone(&counter);

    *h1.borrow_mut() += 1;                 // +1
    for _ in 0..2 { *h2.borrow_mut() += 1; }   // +2
    for _ in 0..3 { *h3.borrow_mut() += 1; }   // +3

    println!("count: {}", counter.borrow());
    println!("owners: {}", Rc::strong_count(&counter));
}
count: 6
owners: 4

Rc::new(RefCell::new(0)) is the shared-mutable pattern: the Rc layer gives multiple owners, the RefCell layer lets any owner mutate the value. Each Rc::clone is a cheap new handle to the same counter, so all increments land on one value (total 6). The owner count is 4: the original counter plus the three handles, none dropped yet. Crucially, the RefCell borrows in each *h.borrow_mut() += 1 don't overlap (each ends at its semicolon), so no BorrowMutError, holding two borrow_mut()s across the same moment would panic. This is single-threaded shared mutable state; chapter 22 does the same job across threads with Arc<Mutex<T>>, where the borrow tracking becomes locking.

You can now allocate on the heap deliberately, share ownership, mutate shared data, and clean up automatically, the full memory toolkit for single-threaded programs. Chapter 22 takes the leap that no other beginner course makes this gently: running code on multiple threads at once, where Rc becomes Arc, RefCell becomes Mutex, and the data race the borrow checker warned about back in chapter 9 becomes the central enemy that Rust, uniquely, makes impossible to compile.