all_lessons/Rust/15 · Smart pointers and interior mutabilitylesson 16 / 23

Smart pointers and interior mutability — the same law, checked later

Lesson 14 ended on two needs the compile-time law cannot meet: a value with several owners, none sure to go last, and a change made through a handle others share. This lesson meets both without weakening the law — it moves the check. Rc<T> counts owners while the program runs and drops the value at zero. Cell and RefCell let a shared handle change what it reaches — Cell by never lending a reference, RefCell by keeping a borrow flag and panicking the moment the law breaks. Each is chosen per value, in the type, and priced: a counter, a flag, a panic where a compile error used to be, a leak whenever counts form a cycle.

The thesis, here
Nothing in this lesson is an exception to shared XOR exclusive. Where the checker cannot see the whole picture — which owner goes last, which reader is still looking — Rust lets you trade the proof for a check that runs, one value at a time, written in the type.
Linear position
Forced by: Closures showed the three ownership modes at work: move, shared, exclusive. But everything so far has exactly one owner and a borrow law checked before the program runs. What about data that genuinely has several owners, or must change through a shared handle?
New idea: the same law, checked at run time, chosen per value and visible in its type — Rc counts owners, Cell/RefCell let a shared handle change what it reaches, and Weak is an edge that does not own.
Forces next: Rc and RefCell keep the law but check it while the program runs, within one thread. Threads share memory, and the same shared-mutation hazard between them is a data race. How does the law extend across threads?
The plan
Six moves. (1) Give one buffer two owners; derive reference counting from the roads not taken. (2) Change a value through a shared handle: Cell, RefCell, and the primitive beneath. (3) Step counts, guards and cycles in a simulator checked against rustc. (4) Say what a smart pointer is — Deref, DerefMut, Drop — and why the law sees through it. (5) Find counting's blind spot, cycles, and close it with Weak. (6) Weigh the alternatives and meet the thread boundary.

1 · Data with two owners

An editor keeps a document's text in one buffer. The window displays it; the undo history records it; either may be closed first, and the program cannot know which. Lesson 03 says every value has exactly one owner, dropped when that owner's scope ends. Which of the two owns the buffer?

Walk the roads. One owner, one borrower: a borrow must fit inside its owner's region (Lesson 06), so the history could never outlive the window — wrong whenever the window closes first. Two copies: two buffers, and an edit through one never shows in the other; it was meant to be one object. A collector: keep the buffer alive while anything reaches it — Go's answer, and outside Lesson 00's budget. What is left is to count: add one per new owner, subtract one per owner dropped, drop the value at zero — one integer, updated as owners come and go, paid only by values that ask for it.

That is Rc<T>, a reference-counted pointer: a handle to one heap allocation holding the value and its counts. Rc::clone(&doc) makes another owner — an increment, not a copy of the text:

use std::rc::Rc;

fn main() {
    let doc = Rc::new(String::from("draft"));      // one owner: count 1
    let view = Rc::clone(&doc);                     // a second owner: count 2
    println!("count {} same object {}", Rc::strong_count(&doc), Rc::ptr_eq(&doc, &view));
    {
        let history = Rc::clone(&doc);              // count 3
        println!("count {} history reads {history}", Rc::strong_count(&doc));
    }                                               // history dropped: count 2
    drop(view);                                     // count 1
    println!("count {}", Rc::strong_count(&doc));
}                                                   // count 0: the String is dropped
count 2 same object true
count 3 history reads draft
count 1

Rc::ptr_eq confirms one object, and the count, not any one scope, decides the drop: the String goes once, with the last of three owners, whatever the order. The price sits in the allocation: measured with a spy allocator, Box::new(0u64) asks for 8 bytes and Rc::new(0u64) for 24 — 16 more, room for the strong count above and a weak count §5 needs — while the handle stays 8 bytes, like a Box. The counts are plain, non-atomic integers: cheap, and the reason an Rc must stay on one thread (§6).

The measuring program (its unsafe is Lesson 19's business) — rustc 1.98.1, aarch64 (Apple silicon), 2026-09
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::{Cell, RefCell};
use std::mem::size_of;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

static LAST: AtomicUsize = AtomicUsize::new(0);   // the size of the last heap request
struct Spy;
unsafe impl GlobalAlloc for Spy {
    unsafe fn alloc(&self, l: Layout) -> *mut u8 { LAST.store(l.size(), Relaxed); unsafe { System.alloc(l) } }
    unsafe fn dealloc(&self, p: *mut u8, l: Layout) { unsafe { System.dealloc(p, l) } }
}
#[global_allocator]
static A: Spy = Spy;

fn main() {
    let b = Box::new(0u64);
    let boxed = LAST.load(Relaxed);
    let r = Rc::new(0u64);
    println!("heap: Box {boxed} Rc {}", LAST.load(Relaxed));
    println!("size: Box {} Rc {} i32 {} Cell {} RefCell {}", size_of::<Box<u64>>(), size_of::<Rc<u64>>(),
        size_of::<i32>(), size_of::<Cell<i32>>(), size_of::<RefCell<i32>>());
    drop((b, r));
}
heap: Box 8 Rc 24
size: Box 8 Rc 8 i32 4 Cell 4 RefCell 16

What the count cannot do is hand out &mut: if two owners could each take exclusive access, one could grow the buffer while the other holds a name into it — Lesson 01's hazard again. C++ lets you write it, with the shared_ptr of the C++ track's Lesson 07:

auto doc = std::make_shared<std::vector<std::string>>(1, "hello");
auto history = doc;                          // a second owner
for (const std::string& line : *doc)         // an iterator into the buffer
    history->push_back(upper(line));         // may reallocate under it: undefined

In Rust the same program stops at the compiler, because an Rc gives every owner shared access and nothing else:

use std::rc::Rc;

fn main() {
    let doc = Rc::new(vec![String::from("hello")]);
    let history = Rc::clone(&doc);                  // a second owner
    for line in doc.iter() {
        history.push(line.to_uppercase());          // a writer through a shared handle
    }
}
error[E0596]: cannot borrow data in an `Rc` as mutable
 --> src/main.rs:7:9
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Vec<String>>`

Many owners, all readers: the law holds by construction. But the editor is stuck — two owners, and neither may change the buffer.

2 · Changing a value through a shared handle

A shared reference only reads, for a reason: a write under a live reader is how Lesson 01's bugs happen. Yet shared things must change — the editor's buffer, a cache, the call counter Lesson 14's Fn closure could not keep, because Fn sees its captures through &self. Three roads: restructure until the writer is the only name (try it first, §6); let every shared reference write (C++, and the end of the promise); or keep the law and let a type enforce it while the program runs — interior mutability, changing contents through a shared reference.

Cell — never lend a reference

A write is harmless if no name points into the value. Cell<T> never hands out a reference to its contents: get copies the value out (so it exists only for Copy types), set moves one in, replace and take swap. With no reference to the inside anywhere, no reader exists for a write to hurt — the law holds with nothing to check, and Lesson 14's counter becomes legal:

use std::cell::Cell;

fn call_twice(f: impl Fn()) { f(); f(); }       // Fn: the closure only gets &self

fn main() {
    let calls = Cell::new(0);                    // no `mut` anywhere
    let counted = || calls.set(calls.get() + 1); // a write through a shared capture
    call_twice(&counted);
    call_twice(&counted);
    println!("called {} times", calls.get());
}
called 4 times

Picture the method Cell refuses to have, fn peek(&self) -> &T: then let r = title.peek(); followed by title.set(String::new()) would leave r reading freed text. Leaving that method out is the whole design. Cell<i32> is 4 bytes, like an i32 (measured above): no flag, no check. The limit: you can never look at the contents in place.

RefCell — lend guarded references, and count them

To iterate a buffer's lines, a reference must escape — so the law must be checked. RefCell<T> checks it at run time. borrow() returns a Ref<T> guard that acts as a shared reference, borrow_mut() a RefMut<T> that acts as an exclusive one; the cell's borrow flag counts the guards alive, and dropping a guard releases it. Many readers or one writer — shared XOR exclusive, enforced by a counter instead of a proof, for a price: RefCell<i32> is 16 bytes (§1) and every borrow checks the flag. Break the law and the call panics; try_borrow and try_borrow_mut return an Err instead. Here is §1's program with the buffer in a RefCell:

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

fn main() {
    let doc = Rc::new(RefCell::new(vec![String::from("hello")]));
    let history = Rc::clone(&doc);                 // a second owner
    for line in doc.borrow().iter() {              // a reader, alive for the whole loop
        history.borrow_mut().push(line.to_uppercase());   // a writer: the law says no
    }
}

It compiles, runs, and panics: RefCell already borrowed. The borrow() in the for line made a reader that lives for the whole loop, and borrow_mut() asked for the writer while it lived. The C++ loop of §1 reallocates under its iterator and carries on into undefined behavior; the panic is the promise kept — a defined stop at the moment the law breaks, before any buffer moves. The repair is the one the static checker would have forced — end the reader before the writer starts:

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

fn main() {
    let doc = Rc::new(RefCell::new(vec![String::from("hello")]));
    let history = Rc::clone(&doc);
    let upper: Vec<String> = doc.borrow().iter().map(|l| l.to_uppercase()).collect();
    history.borrow_mut().extend(upper);      // the reader ended on the line above
    println!("{:?}", doc.borrow());
}
["hello", "HELLO"]
Reading the error
Write the loop with plain references and the checker refuses it before it runs:
fn main() {
    let mut lines = vec![String::from("hello")];
    for line in lines.iter() {
        lines.push(line.to_uppercase());
    }
}
error[E0502]: cannot borrow `lines` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:9
  |
3 |     for line in lines.iter() {
  |                 ------------
  |                 |
  |                 immutable borrow occurs here
  |                 immutable borrow later used here
4 |         lines.push(line.to_uppercase());
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
The compile-time message names three points: the reader's creation, the conflicting writer, the later use. The run-time message names one — RefCell already borrowed, at the borrow_mut line: the law fired, and this call fired it. It cannot say which earlier guard is still alive or where it was made — the cell keeps a count, not a history — so finding the reader (here, a temporary the for keeps alive) is your job; the widget below marks it. Neither message can say whether to end the reader sooner, copy the data, or restructure: that is design.
UnsafeCell — the one primitive beneath both

Under both sits one type. The Reference says UnsafeCell<T> is the only permitted way to change data behind a shared reference; any other way — turning a &T into a &mut T, say — is undefined behavior (Lesson 01's optimizer relied on a shared target not changing). Cell, RefCell, Mutex and the atomics are built on it. It checks nothing: its get returns a raw pointer, and using that takes unsafe code whose author proves the law by hand (Lessons 19–20). Three forms, three places for the law: an API with nothing to check, a flag checked at every borrow, a human's proof.

Put §1 and §2 together and you get the pattern Rc<RefCell<T>>, several owners (a count) sharing a value they can change (a flag) — the pairing the std docs describe, with Arc<Mutex<T>> as its thread-safe analogue (Lesson 17). Counts and flags change while the program runs; to predict them, step through a run.

3 · Counts, guards, and cycles, step by step

The simulator runs scripts on Rc<RefCell<Node>> values; a Node has a next: Option<Rc<…>> edge, a back: Option<Weak<…>> edge (§5) and a Drop that prints its name. Each operation declares a binding — strong handles h1…, weak handles w1…, guards g1… — or drops one; the closing brace drops the rest, last declared first, and a panic ends the scope the same way. The engine only applies counting and flag rules, so tools/rust_verify/15_rcsim.js holds it to rustc 1.98.1: 492 scripts — the 11 this lesson mentions, 400 random, 81 that must fail to compile — and 0 mismatches. Predict before you drag.

Counts, guards, and cycles
Pick a script and drag the step slider: counts and flags per allocation, a panic in red with a ring on the guard in the way, and, after the brace, what is still alive is leaked. Buttons extend the script, acting on on (and to, for edges).
values alive
—
guards alive
—
run
—
leaked at }
—
Show the core JS
function decStrong(st, a, ev) {                   // one strong owner fewer; at zero the value is dropped
  var A = st.allocs[a];
  if (--A.strong === 0) {
    A.alive = false; ev.push(A.name);            // Node's Drop prints first, then its fields drop:
    var n = A.next, w = A.back; A.next = -1; A.back = -1;
    if (n >= 0) decStrong(st, n, ev);            //   next: an Rc — may cascade
    if (w >= 0) st.allocs[w].weak--;             //   back: a Weak — never drops anything
  }
}
// One operation. Returns 'ok' | 'some' | 'none' | { panic: message }.
function apply(st, op, ev) {
  var t = op.t ? find(st, op.t) : null, A = t && t.a >= 0 ? st.allocs[t.a] : null, u, old;
  switch (op.k) {
    case 'new':   st.allocs.push({ name: R.NAMES[st.allocs.length], strong: 1, weak: 0, readers: 0, writer: false, alive: true, next: -1, back: -1 });
                  bind(st, 'h', st.allocs.length - 1); return 'ok';
    case 'clone': A.strong++; bind(st, 'h', t.a); return 'ok';
    case 'down':  A.weak++;   bind(st, 'w', t.a); return 'ok';
    case 'up':    if (A.strong > 0) { A.strong++; bind(st, 'h', t.a); return 'some'; }
                  bind(st, 'h', -1); return 'none';
    case 'bor':   if (A.writer) return { panic: R.MSG_SHR };
                  A.readers++; bind(st, 'g', t.a, false, t.name); return 'ok';
    case 'mut':   if (A.writer || A.readers > 0) return { panic: R.MSG_MUT };
                  A.writer = true; bind(st, 'g', t.a, true, t.name); return 'ok';
    case 'link':  u = find(st, op.u);
                  if (A.writer || A.readers > 0) return { panic: R.MSG_MUT };   // the temporary borrow_mut() fails
                  if (op.weak) { st.allocs[u.a].weak++; old = A.back; A.back = u.a; if (old >= 0) st.allocs[old].weak--; }
                  else { st.allocs[u.a].strong++; old = A.next; A.next = u.a; if (old >= 0) decStrong(st, old, ev); }
                  return 'ok';
    case 'cut':   if (A.writer || A.readers > 0) return { panic: R.MSG_MUT };
                  old = A.next; A.next = -1; if (old >= 0) decStrong(st, old, ev); return 'ok';
    case 'drop':  release(st, t, ev); return 'ok';
  }
}

What to try. The default script is §2's program in miniature. At step 3 A has strong count 2 and one reader, g1; step 4 asks h2 for the writer and panics with RefCell already borrowed, and the ring lands on g1 — the guard the message could not name. At step 5 the scope unwinds — g1, h2, h1 — and drop A prints once, when the count reaches zero. Undo, drop g1, then borrow_mut on h2: no panic. Load a cycle leaks: after step 4 both counts are 2; at the brace each falls to 1 and stops, and both leak — unless you add h2.next = None first. In a Weak back-edge, A has weak count 1 and strong count 1, so the brace drops A, and A's next takes B along. Last, drop the handle a guard borrows: nothing runs — g1 borrows h1, so drop(h1) is rejected at compile time. The flag took over only reader-versus-writer; that a guard cannot outlive its handle is still proven before the program runs. How can the compiler see through a guard?

4 · What a smart pointer is: Deref, DerefMut, Drop

That question has two older relatives: Lesson 05 accepted a &String where a &str was expected and deferred the mechanism, and §1's help line said an Rc lacks DerefMut. All three are one mechanism. A smart pointer is a type that acts like a reference to something it manages, and the compiler knows three traits for it: Deref (fn deref(&self) -> &Target), DerefMut (fn deref_mut(&mut self) -> &mut Target) and Drop. Make the calls visible with a pointer that prints on every deref:

use std::ops::{Deref, DerefMut};

struct Logged<T>(T);                        // a smart pointer that reports every deref
impl<T> Deref for Logged<T> {
    type Target = T;
    fn deref(&self) -> &T { println!("deref"); &self.0 }
}
impl<T> DerefMut for Logged<T> {
    fn deref_mut(&mut self) -> &mut T { println!("deref_mut"); &mut self.0 }
}
fn shout(s: &str) -> String { s.to_uppercase() }

fn main() {
    let mut title = Logged(String::from("draft"));
    let n = title.len();                    // method call: auto-deref inserts deref(&title)
    let loud = shout(&title);               // coercion: &Logged<String> -> &String -> &str
    title.push('!');                        // a write: deref_mut(&mut title)
    let copy = (*title).clone();            // *title means *Deref::deref(&title)
    println!("{n} {loud} {copy}");
}
deref
deref
deref_mut
deref
5 DRAFT draft!

Four expressions, four calls the compiler inserted. title.len(): method lookup found no len on Logged, dereferenced, and tried again — auto-deref. shout(&title): a &Logged<String> stood where a &str was wanted, so the compiler applied deref, then String's own Deref to str — deref coercion, the chain that also turns &Rc<String> into &str. title.push('!') needs &mut String: deref_mut. And *title, on a type that is not a reference, is *Deref::deref(&title) — the fourth line is that call.

Each inserted call's signature ties its result to &self or &mut self (Lesson 08's elision), so the law sees straight through: a smart pointer is a path through places — handle, allocation, value — and the law governs every step. Ref and RefMut are smart pointers too (hence history.borrow_mut().push(…)), and a guard borrows the handle it came from. End the owner while the guard still reads:

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

fn main() {
    let doc = Rc::new(RefCell::new(vec![String::from("hello")]));
    let reader = doc.borrow();           // a guard: it borrows doc
    drop(doc);                           // end the owner while the guard lives?
    println!("{}", reader.len());
}

Accepted, reader would point into an allocation drop(doc) had just freed. §1's help line now reads as a design decision: Rc implements Deref, so any owner may read, and not DerefMut, because nothing in the type says an owner is alone. Box is special to the compiler, not only a library type — *b is a place you may move out of:

use std::rc::Rc;

fn unbox(b: Box<String>) -> String { *b }    // Box is special: *b is a place you can move out of
fn unrc(r: Rc<String>) -> String { *r }      // other owners may still need the String

Moving the text out of an Rc would leave its other owners holding nothing: cannot move out of an `Rc`. The third trait is Lesson 03's destructor, Drop: dropping an Rc lowers the count, and at zero the value's own Drop runs — the widget's drop A. Which is how counting's blind spot shows: sometimes that line never prints.

5 · Counting's blind spot: cycles, and Weak

An outline is a tree: a section owns its subsections, and a subsection wants to reach its parent. Make both edges Rc and the two own each other; drop every outside handle and each count falls to 1 — the other's edge — and stops:

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

struct Node { name: &'static str, next: RefCell<Option<Rc<Node>>> }
impl Drop for Node {
    fn drop(&mut self) { println!("drop {}", self.name); }
}

fn main() {
    {
        let a = Rc::new(Node { name: "a", next: RefCell::new(None) });
        let b = Rc::new(Node { name: "b", next: RefCell::new(Some(Rc::clone(&a))) });
        *a.next.borrow_mut() = Some(Rc::clone(&b));      // close the cycle: a -> b -> a
        println!("a strong={} b strong={}", Rc::strong_count(&a), Rc::strong_count(&b));
    }                                                     // a and b go out of scope ...
    println!("after the scope");                          // ... and no "drop" line appears
}
a strong=2 b strong=2
after the scope

No drop line, ever: a leak. It is safe — nothing can reach the memory, so nothing can misuse it; Lesson 03 put leaks outside undefined behavior, and the Book says plainly that Rust does not promise their absence. But it is a real bug, and counting cannot see it: a tracing collector asks whether anything still reaches the pair and would reclaim it, while a count only knows how many edges point in. That blind spot is the price of drops at a known moment.

Three roads again. Break the cycle by hand before the last handle goes (set next to None): correct, and a manual free in disguise — forget it once and it leaks. Add a cycle collector: a run-time system, outside the budget. Or make one edge of every cycle not own: Weak<T>. Rc::downgrade(&rc) makes one and raises the weak count; a Weak does not keep the value alive; upgrade() returns Option<Rc<T>> — Some while the value lives, None after (absence as a type, Lesson 09). Parents own children with Rc; children observe parents with Weak:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Section { title: &'static str, parent: RefCell<Weak<Section>>, kids: RefCell<Vec<Rc<Section>>> }
impl Drop for Section {
    fn drop(&mut self) { println!("drop {}", self.title); }
}
fn section(title: &'static str) -> Rc<Section> {
    Rc::new(Section { title, parent: RefCell::new(Weak::new()), kids: RefCell::new(vec![]) })
}

fn main() {
    let intro = section("intro");
    let goals = section("goals");
    intro.kids.borrow_mut().push(Rc::clone(&goals));        // strong: the parent owns the child
    *goals.parent.borrow_mut() = Rc::downgrade(&intro);     // weak: the child only observes
    println!("intro strong={} weak={}", Rc::strong_count(&intro), Rc::weak_count(&intro));
    println!("parent: {:?}", goals.parent.borrow().upgrade().map(|p| p.title));
    drop(intro);                                            // the last owner of intro
    println!("parent: {:?}", goals.parent.borrow().upgrade().map(|p| p.title));
}
intro strong=1 weak=1
parent: Some("intro")
drop intro
parent: None
drop goals

intro has one owner and one observer. While it lives, the child's upgrade succeeds; drop(intro) runs its destructor at once — the weak edge did not hold it — and the same upgrade returns None. A Weak still keeps the allocation's bookkeeping, not the value, which is why the widget can show a dropped value whose box waits for the last Weak. With Rc, RefCell and Weak, any graph can be written in safe Rust; whether it should be is the last question.

6 · The law by other means — and choosing

One way to share needs no counting: stop making edges references. Keep every node in one Vec and let an edge be an index, a usize. An index is data, not a borrow; the only borrow is of the Vec, for one expression, so the compile-time law applies unchanged and nothing is checked at run time beyond the bounds check you already pay:

struct Section { title: &'static str, parent: Option<usize> }   // a parent is an index: plain data

fn main() {
    let mut outline = vec![
        Section { title: "intro", parent: None },
        Section { title: "goals", parent: Some(0) },
    ];
    let goals = 1;                                     // a handle is a number, not a borrow
    outline[0].title = "overview";                     // change freely: nothing is borrowed
    let up = outline[goals].parent.unwrap();
    println!("{} is under {}", outline[goals].title, outline[up].title);
    outline.swap_remove(0);                            // delete "overview"; "goals" moves into slot 0
    println!("slot {goals}: {:?}", outline.get(goals).map(|s| s.title));   // the old id finds nothing
    let up = outline[0].parent.unwrap();
    println!("goals is under {}", outline[up].title);  // its parent id now names itself
}
goals is under overview
slot 1: None
goals is under goals

Change is free — nobody is borrowing. But read the last two lines: deleting a node moved another into its slot, so the old id finds nothing and goals' parent id now names goals itself. That is the malloc/free problem in miniature. A stale index is a dangling pointer that cannot cause undefined behavior — it reads the wrong element or fails a bounds check — but it is still a bug, and slot reuse (a free list, a placeholder) is yours to manage.

The situationReach forWhat is checked, and whenThe price
one owner; others look for a whileownership and borrows (Lessons 03–08)the law, at compile timesome restructuring
a graph with deletions, or a hot loop over nodesindices into a Vecbounds, at run timestale ids are your bug to prevent
state truly shared and changed, one threadRc<RefCell<T>>, Weak back edgescounts and flags, at run timecounts, a flag, a panic path, leaks without Weak
the same, across threadsArc<Mutex<T>> (Lessons 16–17)atomic counts and a lockatomic operations, and waiting

The std docs agree: inherited mutability — an owner, or a &mut — is preferred, and interior mutability is "something of a last resort". The sibling tracks price the alternatives: the C++ track's Lesson 07 itemizes shared_ptr's bill, an atomic count among it, since a copy might cross a thread; Go's collector (Go track, Lesson 03) asks about reachability, so a dead cycle is garbage like any other, and the whole program pays for asking. Rust pays only where you write Rc, and keeps it cheap by keeping it on one thread.

What this does not cover · threads
Arc<T> is Rc with an atomic count: thread-safe, at the cost of slower atomic operations, and std advises plain Rc when nothing crosses a thread. Why a separate type and not just a safer Rc? Because the compiler must be able to refuse this:
use std::rc::Rc;
use std::thread;

fn main() {
    let doc = Rc::new(String::from("draft"));
    thread::spawn(move || println!("{doc}")).join().unwrap();
}
The message: `Rc<String>` cannot be sent between threads safely. Which types may cross a thread, and how the compiler knows unprompted, is Lesson 16's derivation.

Common mistakes / failure modes

"Rc is a garbage collector, and Rust prevents leaks"
An Rc drops its value when the count reaches zero and never asks about reachability, so a cycle of Rcs is never dropped (§5): a leak — safe, outside undefined behavior, and not one Rust promises to prevent.
"RefCell breaks the borrow rules — & meant immutable"
& means shared and &mut exclusive; through Cell, RefCell or Mutex a shared reference may change what it reaches. RefCell enforces the same law at run time, by your choice, for a flag and a panic path (§2).
"Rc<RefCell<T>> is how you write normal Rust"
It is the fallback — std calls interior mutability a last resort. Prefer ownership and borrowing, then indices; share ownership only when it really is shared (§6).
"Weak is optional politeness"
It is how a cycle avoids leaking: with a strong back edge the pair is never dropped; with a Weak one the parent's drop takes the child along (§5 and the widget).
"Cell and RefCell are the same thing"
Cell never lends a reference — values move in and out, it costs nothing, it cannot fail. RefCell lends guarded references, keeps a flag, and panics on a conflict (§2).
"clone() on an Rc copies the data"
It adds one to the strong count and returns another handle to the same allocation; Rc::ptr_eq says true (§1).

Checkpoint exercise

Try it
In the widget, build a ring: press Rc::new three times, then add next edges h1→h2, h2→h3, h3→h1 (on, to, then the .next button). Before stepping to the brace, write down each final strong count and what leaks. Press undo and make the last edge a back edge instead: write down the order of the drop lines, and why only one of the three handle drops brings a count to zero. Undo once more, add g1 = h3.borrow(), and re-add the back edge: which operation panics, with which message, and does anything leak now? Check each answer against the readout. (Answer: every count is 2 and all three leak. With the back edge the brace prints drop A, drop B, drop C: only h1 is A's last owner, and A's drop cascades. With the guard, the back edge itself panics with RefCell already borrowed, the ring is never closed, and nothing leaks.)

Where this points next

Everything here kept the law and moved its check into the running program: a count that says when to drop, a flag that says who may write. Both are ordinary integers updated with ordinary loads and stores — sound only because one thread does the updating. Two threads cloning one Rc at once would both write its count: Lesson 01's hazard with the ordering taken away, which between threads is a data race. How, then, do the law and its checker reach across threads?

Takeaway
When one owner is not enough, Rust moves the law's check to run time — per value, in the type — instead of relaxing it. Rc<T> counts owners, drops the value at zero and hands out only shared access. Cell<T> allows change through & by never lending a reference; RefCell<T> lends guards and counts them — many readers or one writer — and panics where the static checker would have refused; UnsafeCell<T>, beneath both, checks nothing. Smart pointers are Deref/DerefMut/Drop implementations, so the law sees through every inserted *. Counting cannot free a cycle — a safe, real leak — so back edges are Weak. Prefer ownership, then indices; Rc<RefCell<T>> is the priced, single-threaded fallback.

Interview prompts

Companion reads: C++ · 07 Ownership and smart pointers (shared_ptr's bill, the weak_ptr fix), C++ · 05 RAII (the destructor behind Drop), Go · 03 Pointers, the heap and the GC (the collector that does all this).