Hedronite Lesson · Polyglot-Dev / Rust · Fri 2026-09-25

Smart pointers: Box, Rc, RefCell

A reference borrows. A smart pointer owns and carries a rule. Name the rule before you pick the type.

Lesson Class: Duha (Rust language track · Beat A)
Focus: Box / Deref / Drop / Rc / RefCell · the Buy Table · the Weak Cut
Code Blocks: clean blocks, explanation in prose
Done-criteria: Say what Box/Rc/RefCell buy, and why Deref/Drop exist
Grounding: TRPL stable ch15-00..06 · Blandy 2017 Ch.9 Interior Mutability (pp.205–209) second cite
The Buy Table
Box buys a known size; Rc buys shared ownership; RefCell buys mutation behind &self.
The Two Traits
Deref makes a struct read like a pointer; Drop makes it clean up like an owner.
The Weak Cut
Edges that own are Rc; edges that point back are Weak.
A reference borrows. A smart pointer owns and carries a rule.

<!-- hal:authoritative:yaml -->

A reference borrows. A smart pointer owns and carries a rule. Name the rule before you pick the type.

§I - Frame

Duha session 17. Topics #17, TRPL Chapter 15: smart pointers. Chapter 4 gave you &T: a pointer with no overhead and no powers. Chapter 15 gives you structs that act like pointers and add one capability each. The chapter's thesis fits in one line: a smart pointer is a struct that implements Deref and Drop.

Three moves land by the end:

  1. The Buy Table: say in one clause what Box<T>, Rc<T>, and RefCell<T> each buy you, and what each costs.
  2. The Two Traits: say why Deref exists (so * and & work through your struct) and why Drop exists (so cleanup runs without a call site).
  3. The Weak Cut: name the cycle that Rc plus RefCell can build, and the Weak<T> edge that breaks it.

Done-criteria: Can say what Box/Rc/RefCell buy, and why Deref/Drop exist.

Threads stay out. Arc and Mutex belong to Ch.16 (Topics #18 and #19).

§II - Box<T>: a known size for an unknown one

A box puts the value on the heap and leaves a pointer on the stack. It has no runtime cost beyond that allocation. The book gives three uses: a type whose size the compiler cannot know, a large value you want to move without copying, and a value you know only by the trait it implements (trait objects, Ch.18).

The first use is the one that compiles or fails. A cons list that holds itself has infinite size:

enum List {
    Cons(i32, List), // error[E0072]: recursive type `List` has infinite size
    Nil,
}

The compiler's own hint names the fix: insert indirection.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

A Box<List> is one pointer wide, so Cons now has a fixed size: an i32 plus a pointer. Box buys a known size. It costs one heap allocation and nothing more.

§III - The two traits: Deref and Drop

**Deref** lets * follow your struct the way it follows &. The book builds MyBox<T>(T), watches *y fail with E0614, then adds one method:

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

Now *y compiles, because Rust rewrites it as *(y.deref()). deref returns a reference, so the inner value stays owned by the box. The same trait powers deref coercion: pass &MyBox<String> where a &str is expected and the compiler chains deref calls (MyBox<String> to String to str) at compile time. No runtime cost.

**Drop** runs your code when the value leaves scope. Write it once on the type, and every exit path gets it:

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _a = Guard("a");
    let b = Guard("b");
    drop(b);            // std::mem::drop: early, by value
    let _c = Guard("c");
}   // prints: drop b, then drop c, then drop a

Two rules show in that output. Values drop in reverse order of creation. You cannot call b.drop() directly (E0040, explicit destructor call); you pass the value to std::mem::drop, which takes ownership so the automatic drop cannot run a second time.

Thus the definition closes: Deref makes the struct read like a pointer, and Drop makes it clean up like an owner.

§IV - Rc<T> and RefCell<T>: two rules moved

**Rc<T>** moves the ownership rule from one owner to a count. Two lists cannot both own a Boxed tail (E0382, use of moved value). They can share an Rc tail:

use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a)); // strong_count(&a) == 2
    {
        let c = Cons(4, Rc::clone(&a)); // == 3
    }
    // c dropped: == 2
}

Rc::clone bumps a counter; it copies no data. Drop decrements it. The value goes when the count hits zero. Rc gives shared reads only, and it is single-threaded.

**RefCell<T>** moves the borrow rule from compile time to run time. One owner still, but borrow() and borrow_mut() are checked when they execute. Break the one-writer-or-many-readers rule and the program panics instead of failing to compile. The book's use: a mock Messenger whose send(&self, ...) must record into a RefCell<Vec<String>>, because the trait signature only grants &self. Blandy (Ch.9) shows the same need in production form: a log_file: RefCell<File> written from a &self method, and Cell<T> for plain Copy values that need only get/set.

The Buy Table, straight from the book's recap:

TypeOwnersBorrows checkedBuysCosts
Box<T>onecompile timeknown size, cheap moves, trait objectsone allocation
Rc<T>manycompile time, shared onlyshared ownership when the last reader is unknowna count, single-thread only
RefCell<T>onerun timemutation behind &selfa panic if you are wrong

§V - The Weak Cut

Combine them and you can leak. Rc<RefCell<...>> nodes that point at each other keep each other's count above zero forever. The book's cons-list cycle ends with both counts stuck at 1 after main returns. The book is plain about it: leaks are memory safe, and preventing them sits outside Rust's guarantees.

The cut is an edge that does not own. Rc::downgrade(&rc) returns a Weak<T> and raises weak_count, which never keeps the value alive. To use it, call upgrade() and match the Option<Rc<T>>:

match parent.upgrade() {
    Some(p) => println!("parent is {}", p.value),
    None => println!("parent already dropped"),
}

The book's tree follows this rule: children are Rc (parents own children), and the parent link is Weak (children do not own parents). If an edge expresses ownership, make it Rc. If it only points back, make it Weak.

§VI - Proof and close

  1. Write the E0072 cons list, fix it with Box, and say what size Cons now has.
  2. Implement Deref on MyBox<T> and explain *(y.deref()).
  3. Implement Drop on a guard, call std::mem::drop on one value, and predict the print order.
  4. Share a tail with Rc::clone and state strong_count at each step.
  5. Fill the Buy Table from memory, then name which edge in a parent/child tree must be Weak.

Done-criteria: Can say what Box/Rc/RefCell buy, and why Deref/Drop exist.

Next: threads and message passing (Topics #18, TRPL Ch.16). Keep the Buy Table; Ch.16 adds Arc and Mutex as its thread-safe rows.

Related