6.7 Overview of Smart Pointers

In much of your Rust code, you’ll work with values stored directly on the stack or use standard library collections like Vec<T> and String, which manage their internal heap allocations automatically. However, Rust also provides smart pointers for specific situations requiring more explicit control over heap allocation, different ownership models (like shared ownership), or the ability to bypass certain borrowing rules safely (via runtime checks). Smart pointers are types that act like pointers but have additional metadata and capabilities, often related to ownership, allocation, or runtime checks. They provide abstractions over raw pointers for managing heap-allocated data or implementing these specific ownership patterns. Here’s a brief preview (detailed in Chapter 19):

  • Box<T>: The simplest smart pointer. Owns data allocated on the heap. Used for transferring ownership of heap data, creating recursive types (whose size would otherwise be infinite), or storing fixed-size handles to dynamically sized types (like trait objects).
    fn main() { // Added main wrapper for editable block
        let b = Box::new(5); // Allocates an i32 on the heap, b owns it.
        println!("Box contains: {}", b);
    }
  • Rc<T> (Reference Counting): Allows multiple owners of the same heap data in a single-threaded context. Keeps track of the number of active references; the data is dropped only when the last reference (Rc) goes out of scope. Use Rc::clone(&rc) to create a new reference and increment the count (this is cheap, just updates the count, not a deep copy).
    use std::rc::Rc;
    fn main() { // Added main wrapper for editable block
        let data = Rc::new(String::from("shared data"));
        let owner1 = Rc::clone(&data); // owner1 shares ownership
        let owner2 = Rc::clone(&data); // owner2 also shares ownership
        // Rc::strong_count shows the number of Rc pointers to the data
        println!("Data: {}, Count: {}", data, Rc::strong_count(&owner1)); // Prints 3
    } // owner1 and owner2 go out of scope, then data. Count drops to 0, String is freed.
  • Arc<T> (Atomic Reference Counting): The thread-safe version of Rc<T>. Uses atomic operations for incrementing/decrementing the reference count, allowing safe sharing of ownership across multiple threads.
    // Example requires threads, make non-editable or more complex
    use std::sync::Arc;
    use std::thread;
    fn main() { // Added main wrapper
        let data = Arc::new(vec![1, 2, 3]);
        println!("Initial count: {}", Arc::strong_count(&data)); // Count is 1
        let thread_handle = Arc::clone(&data); // Clone Arc for another thread, count is 2
        let handle = thread::spawn(move || {
            println!("Thread sees count: {}", Arc::strong_count(&thread_handle)); // Count is 2
            // use thread_handle
        });
        println!("Main sees count after spawn: {}", Arc::strong_count(&data)); // Count is 2
        handle.join().unwrap(); // Wait for thread
        println!("Final count: {}", Arc::strong_count(&data)); // Count is 1 after thread finishes
    } // data goes out of scope, count drops to 0, Vec is freed.
  • RefCell<T> and Cell<T> (Interior Mutability): Provide mechanisms to mutate data even through an apparently immutable reference (&T) – this pattern is called interior mutability.
    • RefCell<T> enforces the borrowing rules (one &mut XOR multiple &) at runtime instead of compile time. If the rules are violated, the program panics. Often used with Rc<T> to allow multiple owners to mutate shared data (within a single thread).
    • Cell<T> is simpler, primarily for Copy types. It allows replacing the contained value (.set()) or getting a copy (.get()) even through a shared reference, without runtime checks or panics (as simple replacement of Copy types doesn’t invalidate other references).
    use std::cell::RefCell;
    use std::rc::Rc;
    fn main() { // Added main wrapper for editable block
        let shared_list = Rc::new(RefCell::new(vec![1]));
        let list_clone = Rc::clone(&shared_list);
        // Mutate through RefCell (runtime borrow check)
        shared_list.borrow_mut().push(2);
        list_clone.borrow_mut().push(3);
        // Access immutably (also runtime checked)
        println!("{:?}", shared_list.borrow()); // Prints [1, 2, 3]
    }

These smart pointers offer different strategies for managing memory and ownership, providing flexibility beyond the basic rules while maintaining Rust’s safety guarantees (either at compile-time or runtime).