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. UseRc::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 ofRc<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>
andCell<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
XORmultiple &
) at runtime instead of compile time. If the rules are violated, the program panics. Often used withRc<T>
to allow multiple owners to mutate shared data (within a single thread).Cell<T>
is simpler, primarily forCopy
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 ofCopy
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).