| Contents |

19 Smart pointers

Memory management is a critical aspect of systems programming. C programmers are accustomed to managing memory manually using raw pointers (*T) and functions like malloc() and free(). This approach offers fine-grained control but is notoriously prone to errors like memory leaks, double frees, and use-after-free bugs.

Rust takes a different approach. It strongly encourages stack allocation and employs compile-time-checked references (&T, &mut T) for borrowing data. These references ensure memory safety for many common patterns without requiring manual deallocation. However, certain scenarios require more explicit control over memory allocation, ownership strategies, and lifetime management, particularly when dealing with heap data or shared access. This is where Rust’s smart pointers come into play. In fact, even familiar types like String and Vec<T> are internally implemented as smart pointers, managing heap-allocated memory for dynamic arrays and strings respectively, and automatically deallocating them when they go out of scope.

Smart pointers in Rust are typically structs that wrap some form of pointer (often a raw pointer internally) but provide enhanced behavior and guarantees. They own the data they point to and manage its lifecycle, most notably by automatically handling deallocation when the smart pointer goes out of scope (via the Drop trait). They integrate seamlessly with Rust’s ownership and borrowing rules, providing memory safety guarantees.

This chapter introduces the most common smart pointers in the Rust standard library, explores their use cases, and contrasts them with memory management techniques in C and C++. We will see how they help prevent the memory safety issues endemic to manual memory management while providing necessary flexibility.


19.1 The Concept of Smart Pointers

At its core, a pointer is simply a variable holding a memory address. C relies heavily on raw pointers, requiring meticulous manual management. Rust, in contrast, primarily uses references (&T for shared access, &mut T for exclusive mutable access). References borrow data temporarily without owning it and do not manage memory allocation or deallocation. The Rust compiler statically verifies references to prevent common issues like dangling pointers by ensuring they never outlive the data they refer to.

A smart pointer differs fundamentally because it owns the data it points to (usually on the heap). This ownership implies several key characteristics:

  1. Resource Management: The smart pointer is responsible for cleaning up the resource it manages (typically freeing memory) when it is no longer needed. In Rust, this cleanup happens automatically when the smart pointer goes out of scope, thanks to the Drop trait.
  2. Abstraction: They abstract away the need for manual deallocation calls (like free()). In safe Rust, you generally cannot manually free memory managed by standard smart pointers.
  3. Enhanced Behavior: Many smart pointers add capabilities beyond basic pointing, such as reference counting (Rc<T>, Arc<T>) or enforcing borrowing rules at runtime (RefCell<T>).
  4. Pointer-Like Behavior: They typically implement the Deref and DerefMut traits, allowing instances of smart pointers to be treated like regular references (&T or &mut T) in many contexts (e.g., using the * operator or method calls via automatic dereferencing).

While safe Rust discourages direct manipulation of raw pointers (*const T, *mut T), smart pointers provide high-level, safe abstractions that offer the flexibility needed for heap allocation, shared ownership, and other advanced patterns, all while upholding Rust’s memory safety principles.

19.1.1 The Deref Trait for Pointer Behavior

Smart pointers in Rust, despite being structs, often behave like regular references. This “pointer-like” behavior is enabled by the Deref trait, found in std::ops. By implementing Deref for a custom type, you define how the dereference operator (*) behaves on instances of that type, allowing them to be treated as if they were references to their inner value.

The Deref trait requires a single method: deref. This method takes an immutable reference to self (&self) and returns an immutable reference to the inner data (&Self::Target).

use std::ops::Deref;
struct MyBox<T>(T); // A simple tuple struct acting as a minimal Box equivalent

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T; // Associated type: what we dereference to

    fn deref(&self) -> &Self::Target {
        &self.0 // Return a reference to the inner value
    }
}

In the example above, MyBox<T> is a custom smart pointer that simply wraps a value. By implementing Deref, we enable the * operator on MyBox instances. For example, if let y = MyBox::new(5);, then *y would yield 5, just as it would for a regular reference let y_ref = &5;.

Rust’s compiler transparently applies Deref::deref when the dereference operator * is used on a type that implements Deref. This means *my_box is desugared to *(my_box.deref()). This seamless integration is why smart pointers like Box<T> can be used almost interchangeably with references in many contexts.

Deref Coercions

A powerful consequence of the Deref trait is deref coercion. This feature allows Rust to automatically convert a reference to a type that implements Deref into a reference to the type it dereferences to. This occurs implicitly in function and method calls where the expected parameter type does not exactly match the provided argument type, but the argument’s type implements Deref to the expected type.

For instance, &String can be coerced to &str because String implements Deref<Target = str>. Similarly, &Box<i32> can be coerced to &i32. This reduces the need for explicit dereferencing or type conversions, making Rust code more ergonomic.

fn hello(name: &str) {
    println!("Hello, {name}!");
}

let m = MyBox::new(String::from("Rust"));
hello(&m); // &MyBox<String> is coerced to &String, then to &str

In this example, &m is of type &MyBox<String>. Due to MyBox implementing Deref to String, and String implementing Deref to str, Rust can automatically chain these deref coercions to convert &MyBox<String> to &String, and then to &str, matching the hello function’s parameter.

For mutable contexts, the DerefMut trait provides similar functionality for mutable dereferencing, allowing &mut T to be coerced to &mut U if T implements DerefMut<Target=U>. A mutable reference &mut T can also coerce to an immutable &U if T implements Deref<Target=U>, but the reverse (immutable to mutable) is not permitted due to Rust’s borrowing rules.

19.1.2 The Drop Trait for Resource Cleanup

While Deref handles how smart pointers behave during their lifetime, the Drop trait defines what happens when a value is no longer needed. The Drop trait allows you to customize the cleanup logic that executes automatically when a value goes out of scope. This is Rust’s implementation of the RAII (Resource Acquisition Is Initialization) pattern, crucial for memory safety and resource management.

The Drop trait requires implementing a single method: drop. This method takes a mutable reference to self (&mut self).

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointers created.");
// c and d will be dropped automatically when they go out of scope.
// d is dropped before c, due to reverse order of creation.

In this example, when c and d go out of scope at the end of main, their respective drop methods are automatically called by the Rust compiler. This mechanism is how Box<T> deallocates heap memory, how File handles close file descriptors, and how other smart pointers manage their specific resources without explicit manual calls.

A key aspect of Drop is that you cannot explicitly call the drop method yourself. Doing so would lead to a compile-time error, as Rust’s ownership system ensures drop is called exactly once when a value is no longer used. If you need to force a value to be cleaned up earlier than its natural scope end, you must use the std::mem::drop function (note: drop is a function in std::mem, not the trait method). This function takes ownership of the value, causing it to be dropped immediately.

let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
std::mem::drop(c); // Force c to be dropped now
println!("CustomSmartPointer dropped before the end of main.");

The Deref and Drop traits together form the bedrock of Rust’s smart pointer design, enabling safe, automatic resource management and ergonomic pointer-like interactions, without the pitfalls of manual memory handling or the overhead of a garbage collector.

19.1.3 When Are Smart Pointers Necessary?

Many Rust programs operate effectively using stack-allocated data, references, and standard library collections like Vec<T> or String (which manage their own heap memory internally). However, explicit use of smart pointers becomes necessary in scenarios like:

  1. Explicit Heap Allocation: When you need direct control over placing data on the heap, perhaps for large objects or types whose size cannot be known at compile time.
  2. Shared Ownership: When a single piece of data needs to be owned or accessed by multiple independent parts of your program simultaneously (Rc<T> for single-threaded, Arc<T> for multi-threaded).
  3. Interior Mutability: When you need to modify data through a shared (immutable) reference, using controlled mechanisms that ensure safety (often involving runtime checks).
  4. Recursive or Complex Data Structures: Implementing types like linked lists, trees, or graphs where nodes might refer to other nodes, often requiring pointer indirection (Box<T>, Rc<T>) to define the structure and manage ownership.
  5. Breaking Ownership Rules Safely: Situations where the strict compile-time ownership rules are too restrictive, but safety can still be guaranteed through runtime checks or specific pointer semantics (e.g., reference counting).
  6. FFI (Foreign Function Interface): Interacting with C libraries often involves managing raw pointers, and smart pointers (especially Box<T>) can help manage the lifetime of Rust data passed to or received from C code.

If your program doesn’t face these specific requirements, Rust’s default mechanisms for memory and data access might suffice.


19.2 Smart Pointers vs. References

Distinguishing between references and smart pointers is fundamental:

References (&T and &mut T):

Smart Pointers (e.g., Box<T>, Rc<T>, Arc<T>):

In essence, references are like temporary lenses for viewing data, while smart pointers are wrappers that own and manage data. Both are crucial tools in Rust for writing safe and efficient code.


19.3 Comparison with C and C++ Memory Management

Understanding how Rust’s smart pointers fit into the evolution of memory management helps appreciate their design:

19.3.1 C: Manual Management

19.3.2 C++: RAII and Standard Smart Pointers

19.3.3 Rust: Ownership, Borrowing, and Smart Pointers

Rust’s approach leverages the type system and compiler to prevent errors that require manual diligence or runtime overhead (like garbage collection) in other languages.


19.4 Box<T>: Simple Heap Allocation

Box<T> is the most basic smart pointer, providing ownership of data allocated on the heap. Conceptually, Box<T> is a simple struct that holds a raw pointer to heap-allocated data of type T.

19.4.1 Key Features of Box<T>

  1. Exclusive Ownership: Ensures only one owner exists, aligning with Rust’s default ownership rules but for heap data.
  2. Heap Allocation: The primary way to explicitly put data on the heap in Rust.
  3. Known Size Pointer: A Box<T> always has the size of a pointer, regardless of the size of T. This is crucial for types whose size isn’t known at compile time (like trait objects) or for recursive types.
  4. Indirection: Provides a level of pointer indirection to access the data.
  5. Deref and DerefMut: Implements these traits. Deref allows a Box<T> to be treated like &T (e.g., using * for immutable access or calling methods via automatic deref coercion: my_box.some_method()). If the Box<T> binding itself is mutable (let mut my_box), DerefMut allows treating it like &mut T, enabling mutation of the heap-allocated value (e.g., *my_box = new_value; or my_box.some_mut_method()). It’s worth noting that the single mut keyword on the binding enables both the standard reassignment of the Box itself (my_box = Box::new(...)) and, thanks to DerefMut, the mutation of the value it points to.
  6. Minimal Overhead: Because Box<T> is essentially just a wrapper around a raw pointer, accessing the data via Box<T> involves the same level of indirection as a C pointer. There’s no additional overhead for the pointer access itself compared to a raw pointer, beyond the initial heap allocation cost.

19.4.2 Use Cases and Trade-Offs

Common Use Cases:

  1. Recursive Data Structures: To define types that need to contain pointers to themselves (e.g., nodes in a list or tree), Box<T> breaks the infinite size calculation at compile time by providing indirection with a known pointer size.

    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
  2. Trait Objects: To store an object implementing a specific trait when the concrete type isn’t known at compile time (dyn Trait). Box<dyn Trait> provides the necessary indirection and owns the unknown-sized object on the heap.

  3. Transferring Large Data: Moving a Box<T> is efficient because it only involves copying the pointer itself (which is small and typically resides on the stack or in a register), not the potentially large data structure located on the heap. This is much faster than moving the entire data structure if it were stack-allocated.

  4. Explicit Heap Placement: To avoid placing large data structures on the stack, preventing potential stack overflows, especially in constrained environments or deep recursion.

Trade-Offs:

Example:

    let stack_val = 5; // On the stack

    // Allocate an integer on the heap, owned by a mutable Box binding
    let mut boxed_val: Box<i32> = Box::new(stack_val);

    // The 'mut' allows the binding itself to be reassigned:
    // boxed_val = Box::new(7);
    // This would drop the original Box(5) and point to a new Box(7).

    // Access the value using immutable dereferencing
    println!("Initial value on heap: {}", *boxed_val); // Output: 5

    // Mutate the value on the heap via mutable dereferencing
    // This requires `boxed_val` to be declared with `let mut`
    *boxed_val += 10;
    println!("Mutated value on heap: {}", *boxed_val); // Output: 15

    // You can still work with the mutated value directly
    let added_val = *boxed_val + 10;
    println!("Heap value + 10: {}", added_val); // Output: 25

    // Methods defined on i32 (taking &self) can often be called directly
    // on the Box<i32> due to automatic deref coercion.
    println!("Absolute value on heap: {}", boxed_val.abs()); // Output: 15
    // Note: .abs() takes &self, so deref coercion works seamlessly.
    // Methods taking `self` like checked_add would need
    // explicit deref: (*boxed_val).checked_add(10)

    // `boxed_val` goes out of scope here. Its Drop implementation runs,
    // freeing the heap memory.

Note: For specific advanced scenarios, particularly involving async code or FFI where data must not be moved in memory after allocation, Pin<Box<T>> is used. This provides guarantees about memory location stability.


19.5 Rc<T>: Single-Threaded Reference Counting

Rust’s default ownership model mandates a single owner. What if you need multiple parts of your program to share ownership of the same piece of data, without copying it, and where lifetimes aren’t easily provable by the borrow checker? Rc<T> (Reference Counted pointer) addresses this for single-threaded scenarios.

Rc<T> manages data allocated on the heap and keeps track of how many Rc<T> pointers actively refer to that data. The data remains allocated as long as the strong reference count is greater than zero.

19.5.1 Why Rc<T>?

19.5.2 How It Works

Important Constraints:

Example:

use std::rc::Rc;

#[derive(Debug)]
struct SharedData { value: i32 }

    // Rc manages SharedData on the heap, along with its reference counts
    let data = Rc::new(SharedData { value: 100 });

    // Rc::strong_count is useful for demonstration/debugging
    println!("Initial strong count: {}", Rc::strong_count(&data)); // Output: 1

    // Create two more pointers sharing ownership by cloning the Rc pointer
    let owner1 = Rc::clone(&data); // Increments strong count
    let owner2 = Rc::clone(&data); // Increments strong count

    println!("Count after two clones: {}", Rc::strong_count(&data)); // Output: 3

    // Access data through any owner (dereferences to &SharedData)
    println!("Data via owner1: {:?}", owner1);
    println!("Data via owner2: {:?}", owner2);
    println!("Data via original: {:?}", data);

    drop(owner1); // owner1 goes out of scope, decrements strong count
    println!("Count after dropping owner1: {}", Rc::strong_count(&data)); // Output: 2

    drop(owner2); // owner2 goes out of scope, decrements strong count
    println!("Count after dropping owner2: {}", Rc::strong_count(&data)); // Output: 1

    // The original `data` goes out of scope here. Strong count becomes 0.
    // SharedData is dropped, weak count is decremented.
    // Since weak count also becomes 0, the heap memory is freed.

The function Rc::strong_count(&pointer) provides the current strong reference count. This is primarily useful for debugging, demonstration, or specific resource management checks, but less common in typical application logic.

19.5.3 Limitations and Trade-Offs


19.6 Interior Mutability: Cell<T>, RefCell<T>, OnceCell<T>

Rust’s borrowing rules are strict: you cannot have mutable access (&mut T) at the same time as any other reference (&T or &mut T) to the same data. This is checked at compile time and prevents data races. However, sometimes this is too restrictive. The interior mutability pattern allows mutation through a shared reference (&T), moving the borrowing rule checks from compile time to runtime or using specific mechanisms for simple types.

These types reside in the std::cell module and are generally intended for single-threaded use cases.

19.6.1 Cell<T>: Simple Value Swapping (for Copy types)

Cell<T> offers interior mutability for types T that implement the Copy trait (primitive types like i32, f64, bool, tuples/arrays of Copy types, and simple structs composed of Copy types).

Example:

use std::cell::Cell;

    // `i32` implements Copy
    let shared_counter = Cell::new(0);

    // Can mutate through the shared reference `&shared_counter`
    let current = shared_counter.get();
    shared_counter.set(current + 1);

    shared_counter.set(shared_counter.get() + 1); // Increment again

    println!("Counter value: {}", shared_counter.get()); // Output: 2

19.6.2 RefCell<T>: Runtime Borrow Checking

For types that are not Copy, or when you need actual references (&T or &mut T) to the internal data rather than just copying/replacing it, RefCell<T> is the appropriate choice.

Example:

use std::cell::RefCell;

    // Vec<i32> is not Copy
    let shared_list = RefCell::new(vec![1, 2, 3]);

    // Get an immutable borrow
    {
        // `borrow()` returns Ref<Vec<i32>>, which derefs to &Vec<i32>
        let list_ref = shared_list.borrow();
        println!("First element: {}", list_ref[0]);
        // list_ref goes out of scope here, releasing the immutable borrow
    }

    // Get a mutable borrow
    {
        // `borrow_mut()` returns RefMut<Vec<i32>>, which derefs to &mut Vec<i32>
        let mut list_mut_ref = shared_list.borrow_mut();
        list_mut_ref.push(4);
        // list_mut_ref goes out of scope here, releasing the mutable borrow
    }

    println!("Current list: {:?}", shared_list.borrow());

    // Example of runtime panic: Uncommenting the lines below would cause a panic
    // let _first_borrow = shared_list.borrow();
    // let _second_borrow_mut = shared_list.borrow_mut();
    // PANIC! Cannot mutably borrow while immutably borrowed.

19.6.3 Combining Rc<T> and RefCell<T>

A very common pattern is Rc<RefCell<T>>. This allows multiple owners (Rc) to share access to data that can also be mutated (RefCell) within a single thread, deferring borrow checks to runtime.

Example: Simulating a graph node that can be shared and whose children can be modified.

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

#[derive(Debug)]
struct Node {
    value: i32,
    // Children owned via Rc, but Vec is mutable via RefCell
    children: RefCell<Vec<Rc<Node>>>,
}

    let root = Rc::new(Node {
        value: 10,
        children: RefCell::new(vec![]),
    });

    let child1 = Rc::new(Node { value: 11, children: RefCell::new(vec![]) });
    let child2 = Rc::new(Node { value: 12, children: RefCell::new(vec![]) });

    // Mutate the children Vec through the RefCell, even though `root` is shared via Rc
    // Obtain a mutable borrow of the Vec inside the RefCell
    root.children.borrow_mut().push(Rc::clone(&child1));
    root.children.borrow_mut().push(Rc::clone(&child2));
    // Mutable borrows are released here

    println!("Root node: {:?}", root);
    println!("Child1 strong count: {}", Rc::strong_count(&child1));
    // Output: 2 (root.children + child1 var)

std::cell::OnceCell<T> provides a cell that can be written to exactly once. It’s useful for lazy initialization or setting global configuration within a single thread. After the first successful write, subsequent attempts fail silently. get() returns an Option<&T>.

Related types like std::sync::OnceLock (thread-safe) or types in crates like once_cell provide convenient wrappers for computing a value on first access (lazy initialization).

Example (OnceCell):

use std::cell::OnceCell;

    let config: OnceCell<String> = OnceCell::new();

    // Try to get the value before setting - returns None
    assert!(config.get().is_none());

    // Initialize the config
    let result = config.set("Initial Value".to_string());
    assert!(result.is_ok());

    // Try to get the value now - returns Some(&String)
    println!("Config value: {}", config.get().unwrap());

    // Attempting to set again fails (returns Err containing the value we tried to set)
    let result2 = config.set("Second Value".to_string());
    assert!(result2.is_err());
    println!("Config value is still: {}", config.get().unwrap());
    // Remains "Initial Value"

Summary of Single-Threaded Interior Mutability:


19.7 Arc<T>: Thread-Safe Reference Counting

Rc<T> is unsuitable for multi-threaded environments because its reference count updates are not atomic (not protected against race conditions). When you need to share ownership of data across multiple threads, Rust provides Arc<T> (Atomically Reference Counted).

Arc<T> behaves very similarly to Rc<T> but uses atomic operations for incrementing and decrementing the reference count. These operations guarantee correctness even when performed concurrently by multiple threads, albeit with a higher performance cost than Rc’s non-atomic updates.

19.7.1 Arc<T> Basics

Example: Sharing immutable data across threads.

use std::sync::Arc;
use std::thread;

    // Data wrapped in Arc for thread-safe sharing
    let numbers = Arc::new(vec![10, 20, 30, 40, 50]); // Vec<i32> is Send + Sync
    let mut handles = vec![];

    // Arc::strong_count is useful for demonstration/debugging
    println!("Initial Arc strong count: {}", Arc::strong_count(&numbers)); // Output: 1

    // Spawn multiple threads, each cloning the Arc
    for i in 0..3 {
        let numbers_clone = Arc::clone(&numbers); // Clone Arc, increments atomic count
        let handle = thread::spawn(move || { // `move` takes ownership of numbers_clone
            // Access the shared data immutably from the thread
            println!("Thread {}: Element at index {}: {}", i, i, numbers_clone[i]);
            // numbers_clone dropped here, count decreases atomically
        });
        handles.push(handle);
    }

    // `numbers` still exists in the main thread
    // Count might fluctuate as threads start/finish cloning/dropping
    println!("Count after spawning threads (approx): {}", Arc::strong_count(&numbers));

    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }

    // After all threads finish, only the original `numbers` Arc remains
    println!("Final Arc strong count: {}", Arc::strong_count(&numbers)); // Output: 1
    // `numbers` dropped here, count becomes 0, Vec is dropped, memory freed.

19.7.2 Combining Arc<T> with Mutexes/RwLocks for Shared Mutability

Since Arc<T> only grants immutable access, how do you mutate data shared across threads? You combine Arc<T> with a thread-safe interior mutability primitive, typically std::sync::Mutex<T> or std::sync::RwLock<T>.

Example: Shared counter using Arc<Mutex<T>>

use std::sync::{Arc, Mutex};
use std::thread;

    // Shared counter: Arc for shared ownership, Mutex for exclusive access for mutation
    // 0u32 is Send because u32 is Send
    let counter = Arc::new(Mutex::new(0u32));
    let mut handles = vec![];

    for i in 0..5 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // Lock the mutex to gain exclusive access.
            // .lock() returns Result<MutexGuard<..>, PoisonError<..>>
            // .unwrap() panics if the lock was "poisoned" (a thread panicked
            // while holding it).
            let mut num = counter_clone.lock().unwrap(); // `num` is a MutexGuard<u32>

            // Mutate the data safely (dereferences guard to &mut u32)
            *num += 1;
            println!("Thread {} incremented counter to {}", i, *num);

            // Mutex is automatically unlocked when `num` (the lock guard) goes
            // out of scope here.
        });
        handles.push(handle);
    }

    // Wait for all threads to finish
    for handle in handles {
        handle.join().unwrap();
    }

    // Lock the mutex in the main thread to read the final value
    println!("Final counter value: {}", *counter.lock().unwrap()); // Output: 5

While Mutex provides simple exclusive access, RwLock can be more efficient if the data is read much more often than it’s written, because it allows any number of readers to access the data concurrently. Only write access requires exclusivity.

Example: Shared data using Arc<RwLock<T>>

This example simulates multiple threads reading shared data concurrently, while fewer threads occasionally acquire exclusive access to modify it.

use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;

    // Data shared via Arc, protected by RwLock for read/write access
    // Let's store a simple value, initially 100
    let shared_data = Arc::new(RwLock::new(100));
    let mut handles = vec![];

    println!("Initial data: {}", *shared_data.read().unwrap());

    // --- Spawn multiple reader threads ---
    // These threads can access the data concurrently if no writer holds the lock.
    for i in 0..5 {
        let data_clone = Arc::clone(&shared_data);
        let handle = thread::spawn(move || {
            // Acquire read lock using .read().unwrap()
            // Blocks only if a writer currently holds the lock.
            let data_guard = data_clone.read().unwrap(); // Returns RwLockReadGuard
            println!("Reader {} sees data: {}", i, *data_guard);

            // Simulate some work while holding the read lock
            thread::sleep(Duration::from_millis(50 + (i * 10) as u64));
            // Stagger sleeps

            // Read lock is automatically released when data_guard goes out of scope
            println!("Reader {} finished.", i);
        });
        handles.push(handle);
    }

    // Allow readers to start
    thread::sleep(Duration::from_millis(10));

    // --- Spawn fewer writer threads ---
    // These threads need exclusive access to modify the data.
    for i in 0..2 {
        let data_clone = Arc::clone(&shared_data);
        let handle = thread::spawn(move || {
            // Acquire write lock using .write().unwrap()
            // Blocks if any readers OR another writer holds the lock.
            let mut data_guard = data_clone.write().unwrap();
            // Returns RwLockWriteGuard
            *data_guard += (i + 1) * 10; // Writer 0 adds 10, Writer 1 adds 20
            println!("Writer {} modified data to: {}", i, *data_guard);

            // Simulate some work while holding the write lock
            thread::sleep(Duration::from_millis(100));

            // Write lock is automatically released when data_guard goes out of scope
            println!("Writer {} finished.", i);
        });
        handles.push(handle);
    }

    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }

    // Read the final value (acquires a read lock)
    println!("Final data: {}", *shared_data.read().unwrap());
    // Expected: 100 + 10 (from writer 0) + 20 (from writer 1) = 130

This example demonstrates:

Arc<T> (often combined with Mutex or RwLock) is fundamental for managing shared state safely and effectively in concurrent Rust programs. It comes with the overhead of atomic operations for reference counting and the potential blocking overhead of acquiring locks.


19.8 Weak<T>: Breaking Reference Cycles

Reference-counted pointers (Rc<T>, Arc<T>) track ownership via a strong reference count. The data stays alive as long as the strong count > 0. This works well unless objects form a reference cycle: Object A holds a strong reference (Rc or Arc) to Object B, and Object B holds a strong reference back to Object A.

In such a cycle, even if all external references to A and B are dropped, A and B still hold strong references to each other. Their strong counts will never reach zero, and their memory will leak – it’s never deallocated because their Drop implementations are never called.

Weak<T> is a companion smart pointer for both Rc<T> and Arc<T> designed specifically to break these cycles. A Weak<T> provides a non-owning reference to data managed by an Rc or Arc.

19.8.1 Strong vs. Weak References

When an Rc/Arc is dropped, it decrements the strong count. If the strong count becomes zero, the inner value T is dropped. Then, the weak count is decremented (corresponding to the allocation itself). If the weak count also becomes zero, the heap allocation (holding the counts and formerly T) is freed. Weak<T> pointers only keep the allocation alive (containing the counts) until the last Weak<T> is dropped, even if T itself has already been dropped.

By using Weak<T> for references that would otherwise complete a cycle (e.g., a child referencing its parent in a tree where parents strongly own children), you allow the strong counts to drop to zero when external references disappear, enabling proper deallocation of the contained value T.

19.8.2 Accessing Data via Weak<T>

Since a Weak<T> doesn’t own the data, the data might have been deallocated (if the strong count reached zero) while the Weak<T> still exists. Therefore, you cannot access the data directly through a Weak<T>.

To access the data, you must attempt to upgrade the Weak<T> back into a strong reference (Rc<T> or Arc<T>) using the upgrade() method:

This mechanism ensures you only access the data if it’s still valid.

Consider a tree where nodes own their children (Rc), but children need a reference back to their parent. Using Rc for the parent link would create cycles (parent -> child and child -> parent both strong). Weak solves this:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    // Parent link uses Weak to avoid cycles
    parent: RefCell<Weak<Node>>, // Weak pointer doesn't own the parent
    // Children links use Rc for ownership
    children: RefCell<Vec<Rc<Node>>>, // Rc owns the children
}

    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()), // Start with no parent (empty Weak)
        children: RefCell::new(vec![]),
    });

    println!(
        "Leaf initial: strong={}, weak={}",
        Rc::strong_count(&leaf), // Count for `leaf` variable
        Rc::weak_count(&leaf) // Count related to allocation itself + any Weak pointers
    ); // Output: strong=1, weak=1

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]), //Branch owns leaf (strong ref)
    });

    println!(
        "Branch initial: strong={}, weak={}",
        Rc::strong_count(&branch),
        Rc::weak_count(&branch)
    ); // Output: strong=1, weak=1
    println!(
        "Leaf counts after branch owns it: strong={}, weak={}",
        Rc::strong_count(&leaf), // Now 2: `leaf` var + `branch.children`
        Rc::weak_count(&leaf)
    ); // Output: strong=2, weak=1

    // Set leaf's parent to point to branch using a weak reference
    // Rc::downgrade creates a Weak<Node> from the Rc<Node>
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
    // Increments branch's weak count

    println!(
        "Branch after parent link: strong={}, weak={}",
        Rc::strong_count(&branch), // Strong count unchanged
        Rc::weak_count(&branch)
        // Weak count increments (now 2: allocation + leaf.parent)
    ); // Output: strong=1, weak=2
    println!(
        "Leaf after parent link: strong={}, weak={}",
        Rc::strong_count(&leaf), // Unchanged
        Rc::weak_count(&leaf)
    ); // Output: strong=2, weak=1

    // Access leaf's parent using upgrade()
    if let Some(parent_node) = leaf.parent.borrow().upgrade() {
        // Successfully got a temporary Rc<Node> to the parent
        println!("Leaf's parent value: {}", parent_node.value); // Output: 5
        // `parent_node` (the temporary Rc) drops here, decr. branch's strong count
    } else {
        println!("Leaf's parent has been dropped.");
    }

    // Check counts before dropping branch variable
    println!("Counts before dropping branch var: branch(strong={}, weak={}),
        leaf(strong={}, weak={})",
        Rc::strong_count(&branch), Rc::weak_count(&branch), // branch(1, 2)
        Rc::strong_count(&leaf), Rc::weak_count(&leaf));    // leaf(2, 1)

    drop(branch); // Drop the `branch` variable's strong reference

    println!(
        "Counts after dropping branch var: leaf(strong={}, weak={})",
        Rc::strong_count(&leaf),
        // Leaf strong count drops to 1 (only `leaf` var remains)
        Rc::weak_count(&leaf)    // Leaf weak count remains 1
    ); // Output: leaf(strong=1, weak=1)
    // Note: Branch's strong count became 0, so Node(5) was dropped.
    // Branch's weak count became 1 (due to leaf.parent). Allocation still exists.

    // Try accessing the parent again; Node(5) data should be gone.
    if leaf.parent.borrow().upgrade().is_none() {
        // upgrade() fails because branch's strong count is 0
        println!("Leaf's parent has been dropped (upgrade failed)."); // Should print
    } else {
        println!("Leaf's parent still exists?"); // Should not print
    }

    // leaf drops here, its strong count becomes 0, Node(3) is dropped.
    // Its weak count becomes 0, allocation is freed.
    // When Node(3) drops, its RefCell<Weak<Node>> drops.
    // The Weak pointer to branch drops.
    // Branch's weak count becomes 0, its allocation is freed.

By using Weak<Node> for the parent field, the reference cycle is broken, allowing both branch and leaf nodes (and their allocations) to be deallocated correctly when their strong counts reach zero.


19.9 Summary

Rust’s standard library provides a versatile set of smart pointers that extend its core ownership and borrowing system to handle more complex memory management scenarios safely and efficiently:

These tools enable developers to implement complex data structures, manage shared state, and build concurrent applications without sacrificing Rust’s core promise of memory safety. They replace the need for manual memory management found in C and mitigate issues sometimes encountered with C++ smart pointers (like dangling raw pointers or undetected cycles) by integrating deeply with the borrow checker and employing runtime checks or atomic operations where necessary. Choosing the right smart pointer (or combination) for the specific ownership, mutability, and concurrency requirements is key to writing idiomatic and robust Rust code.



| Contents |