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.
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?
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.
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.
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"]
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.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.
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 situation | Reach for | What is checked, and when | The price |
|---|---|---|---|
| one owner; others look for a while | ownership and borrows (Lessons 03–08) | the law, at compile time | some restructuring |
| a graph with deletions, or a hot loop over nodes | indices into a Vec | bounds, at run time | stale ids are your bug to prevent |
| state truly shared and changed, one thread | Rc<RefCell<T>>, Weak back edges | counts and flags, at run time | counts, a flag, a panic path, leaks without Weak |
| the same, across threads | Arc<Mutex<T>> (Lessons 16–17) | atomic counts and a lock | atomic 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.
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"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"Weak is optional politeness"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"Rc::ptr_eq says true (§1).Checkpoint exercise
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?
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
- When do you use
Box,RcandArc? (§1, §6 —Boxfor one owner of a heap value;Rcwhen several owners keep one value alive and none is sure to go last, at the price of two counts;ArcisRcwith atomic counts, for threads.) - When is the value inside an
Rcdropped? (§1, §3 — when the strong count reaches zero: with the last owner, whichever that is, since aWeakdoes not count. ItsDropruns, then its fields drop, so anextedge may cascade.) - Why can't you get a
&mut Tout of anRc<T>? (§1, §4 — another owner could be reading;RcimplementsDerefbut notDerefMut, so the compiler reportsE0596.) - What is interior mutability, and how do
Cell,RefCellandUnsafeCelldiffer? (§2 — change through a shared reference.Cellnever lends a reference, so nothing needs checking;RefCell'sborrow/borrow_mutreturn guards the cell counts, and a writer while any guard lives panics withRefCell already borrowed;UnsafeCell, the only permitted primitive beneath both, checks nothing.) - How do reference cycles leak, and how does
Weakfix them? (§5 — each node's count is held up by the other's edge; aWeakback edge is not an owner, so the parent's drop cascades to the child, andupgradethen returnsNone.) - What is deref coercion, and does the borrow checker see through it? (§4 — inserted
deref/deref_mutcalls turn&Rc<String>into&str; their signatures tie the result to the pointer, so the law governs the whole path — a guard cannot outlive its handle,E0505.) - When would you use indices into a
Vecinstead ofRc<RefCell<T>>? (§6 — graphs with deletions, hot loops: an index is data, so the static law applies and nothing is counted; a stale index is a logic bug, never undefined behavior.)
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).