Lesson 19

Smart Pointers

Use Box for heap allocation, build recursive types, and share data with Rc.

Box: Putting Data on the Heap

Box<T> is the simplest smart pointer -- it allocates data on the heap and owns it. When the Box goes out of scope, the heap memory is freed. You need Box for recursive types (like linked lists or trees) because the compiler needs to know the size of a type at compile time, and recursive types have infinite size without indirection.

Recursive Types

A linked list node that contains another node has infinite size. Box solves this because a pointer always has a known, fixed size. The classic pattern is: enum List { Cons(value, Box<List>), Nil }. This is how you build tree structures, linked lists, and other recursive data in Rust.

Rc: Shared Ownership

Rc<T> (Reference Counted) lets multiple parts of your program share ownership of the same data. Each Rc::clone() increments a counter; when the last clone is dropped, the data is freed. Note that Rc is for single-threaded scenarios only -- for multi-threaded code, you will use Arc (covered in the next lesson).

Boxes and Rc both implement the Deref trait, meaning you can use them like regular references -- the compiler automatically dereferences them for you.

Try it

use std::rc::Rc;

// A recursive linked list using Box
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

impl List {
    // Build a list from a vector
    fn from_vec(items: &[i32]) -> List {
        let mut list = List::Nil;
        for &item in items.iter().rev() {
            list = List::Cons(item, Box::new(list));
        }
        list
    }

    // Walk the list and collect values
    fn to_vec(&self) -> Vec<i32> {
        let mut result = Vec::new();
        let mut current = self;
        loop {
            match current {
                List::Cons(value, next) => {
                    result.push(*value);
                    current = next;
                }
                List::Nil => break,
            }
        }
        result
    }

    // Sum all values in the list
    fn sum(&self) -> i32 {
        match self {
            List::Cons(value, next) => value + next.sum(),
            List::Nil => 0,
        }
    }
}

fn main() {
    // --- Box<T>: Heap Allocation ---
    println!("--- Box<T> ---");
    let boxed = Box::new(42);
    println!("  Boxed value: {}", boxed);
    println!("  Dereferenced: {}", *boxed);

    // Box is essential for recursive types
    println!("\n--- Linked List with Box ---");
    let list = List::from_vec(&[1, 2, 3, 4, 5]);
    println!("  List: {:?}", list.to_vec());
    println!("  Sum: {}", list.sum());

    let empty = List::Nil;
    println!("  Empty list sum: {}", empty.sum());

    // --- Rc<T>: Shared Ownership ---
    println!("\n--- Rc<T>: Shared Ownership ---");

    // Imagine two lists that share a common tail
    let shared_tail = Rc::new(vec![3, 4, 5]);
    println!("  Reference count after creation: {}", Rc::strong_count(&shared_tail));

    let owner_a = Rc::clone(&shared_tail);
    println!("  Reference count after clone A: {}", Rc::strong_count(&shared_tail));

    let owner_b = Rc::clone(&shared_tail);
    println!("  Reference count after clone B: {}", Rc::strong_count(&shared_tail));

    println!("  Owner A sees: {:?}", owner_a);
    println!("  Owner B sees: {:?}", owner_b);
    println!("  Same data? {}", Rc::ptr_eq(&owner_a, &owner_b));

    // Drop one owner
    drop(owner_a);
    println!("  Reference count after dropping A: {}", Rc::strong_count(&shared_tail));

    drop(owner_b);
    println!("  Reference count after dropping B: {}", Rc::strong_count(&shared_tail));
    // shared_tail is the last owner -- data is still alive

    // --- Box with trait objects ---
    println!("\n--- Box with Trait Objects ---");
    let shapes: Vec<Box<dyn Describable>> = vec![
        Box::new(Circle { radius: 3.0 }),
        Box::new(Square { side: 4.0 }),
        Box::new(Circle { radius: 1.5 }),
    ];

    for shape in &shapes {
        println!("  {}", shape.describe());
    }
}

// Trait objects need Box (or another pointer) because their size
// isn't known at compile time.
trait Describable {
    fn describe(&self) -> String;
}

struct Circle {
    radius: f64,
}

impl Describable for Circle {
    fn describe(&self) -> String {
        format!("Circle with radius {:.1}, area {:.2}", self.radius, std::f64::consts::PI * self.radius * self.radius)
    }
}

struct Square {
    side: f64,
}

impl Describable for Square {
    fn describe(&self) -> String {
        format!("Square with side {:.1}, area {:.2}", self.side, self.side * self.side)
    }
}

Challenge

Add a length() method to the List enum that counts how many elements are in the linked list. Nil should return 0, and each Cons adds 1.

Previous: Modules & Visibility

Next: Concurrency