Level 3 · Power Tools

Smart Pointers 📦🧠

Ownership's rules are strict on purpose, but some programs genuinely need more flexibility. Smart pointers bend the rules safely, each in one specific, well-labeled way.

What's a smart pointer?

A regular reference (&T) just points at data. A smart pointer is a small struct that points at data and manages it: freeing it at the right moment, counting who's using it, or policing access. You've secretly used two already: String and Vec both own heap data and clean it up on scope exit. Today: the three famous ones.

Box<T>: a simple crate for the warehouse 📦

Box moves a value to the heap and owns it there. One owner, freed at scope end; ownership rules unchanged, just heap-flavored:

fn main() {
    let boxed_num = Box::new(5);
    println!("{}", *boxed_num + 1);   // * reaches into the box
}   // box dropped → heap memory freed

Why would you? The killer use case is recursive types. This doesn't compile:

enum TodoList {
    Item(String, TodoList),   // ❌ a list containing a list containing...
    Done,
}

The compiler must know how much memory a type needs, but this type contains itself, so the math never ends ("infinite size", it will say). A Box breaks the spiral, because a box is always just one pointer-size, no matter what's inside:

enum TodoList {
    Item(String, Box<TodoList>),   // ✅ known size: text + one pointer
    Done,
}

use TodoList::{Item, Done};

fn main() {
    let list = Item(String::from("learn Rust"),
               Box::new(Item(String::from("feed Rusty"),
               Box::new(Done))));

    let mut current = &list;
    while let Item(task, next) = current {
        println!("todo: {task}");
        current = next;
    }
}

Rc<T>: shared ownership, counted 🔢

Ownership rule #1 says one owner per value. But consider a song in three playlists: who owns it? All of them, really; it should live until the last playlist is gone. Rc (reference counted) does exactly that: every clone increments a counter, every drop decrements it, and the value is freed when the count hits zero.

use std::rc::Rc;

fn main() {
    let song = Rc::new(String::from("🎵 Crab Rave"));

    let playlist_a = Rc::clone(&song);   // cheap: counts, doesn't copy
    let playlist_b = Rc::clone(&song);

    println!("owners: {}", Rc::strong_count(&song));  // 3
    drop(playlist_a);
    drop(playlist_b);
    println!("owners: {}", Rc::strong_count(&song));  // 1, still alive
}

Note: Rc hands out read-only access; many owners who could all write would be chaos (Lesson 7's law). And it's single-threaded only; its thread-safe twin Arc stars in the next lesson.

RefCell<T>: the rules, checked at runtime 🕵️

Sometimes you genuinely need to mutate shared data, and you (not the compiler) know it's safe. RefCell moves the borrow checking from compile time to runtime: the same "many readers or one writer" law, enforced while the program runs. Break it and the program panics, pointing at the crime.

use std::cell::RefCell;

fn main() {
    let diary = RefCell::new(String::from("day 1."));

    diary.borrow_mut().push_str(" found kelp.");  // runtime-checked write
    println!("{}", diary.borrow());               // runtime-checked read

    // But two writers at once?
    let _a = diary.borrow_mut();
    // let _b = diary.borrow_mut();   // 💥 PANIC: already borrowed
}

The classic combo is Rc<RefCell<T>>: many owners (Rc) who can each mutate (RefCell), a favorite for graphs and shared game state in single-threaded programs.

🧭 Which one when? Default: plain ownership + borrows; reach for smart pointers only when the compiler forces the issue. Recursive type / "infinite size" error? Box. Several owners, read-only? Rc. Several owners who mutate (one thread)? Rc<RefCell<T>>. Across threads? Next lesson. 😉
⚠️ Common stumbles
  • Using Box "for performance" everywhere: it adds indirection; use it when the type system needs it, not as a lucky charm.
  • Holding a borrow_mut() guard too long, then borrowing again: runtime panic. Keep RefCell borrows short and scoped.
  • song.clone() vs Rc::clone(&song): both work, but Rustaceans write the second to signal "just counting, not deep-copying."
Exercise 1

Break the infinite loop

Type the boxless TodoList into the Playground and read the "infinite size" error, including the compiler's suggested fix. Then apply it and build a three-item list.

Reveal answer

The compiler suggests exactly what this lesson did: "insert some indirection (e.g., a Box, Rc, or &) to break the cycle." Even at the advanced level, error messages remain the best teacher; that never stops being true.

Exercise 2

Count the owners

Create an Rc<Vec<i32>>, clone it twice inside an inner { } scope, printing Rc::strong_count before, during, and after the scope. Predict the three numbers first, then run it.

Reveal solution
use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    println!("before: {}", Rc::strong_count(&data)); // 1
    {
        let _a = Rc::clone(&data);
        let _b = Rc::clone(&data);
        println!("inside: {}", Rc::strong_count(&data)); // 3
    }
    println!("after: {}", Rc::strong_count(&data));  // 1
}

Scope ends → clones dropped → count falls. Deterministic, visible, no garbage collector in sight.

Exercise 3

The shared scoreboard

Two "views" of one scoreboard: create Rc<RefCell<Vec<i32>>>, clone it into view_a and view_b, push a score through each view, then print the vector through the original handle. One value, three names, everybody sees everything.

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

fn main() {
    let scores = Rc::new(RefCell::new(vec![100]));

    let view_a = Rc::clone(&scores);
    let view_b = Rc::clone(&scores);

    view_a.borrow_mut().push(85);
    view_b.borrow_mut().push(92);

    println!("{:?}", scores.borrow());  // [100, 85, 92]
}