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>?

  • Enables multiple owners of the same heap-allocated data within a single thread.
  • Useful when the lifetime of shared data cannot be determined statically by the borrow checker.
  • Avoids costly deep copies of data when sharing is needed.

19.5.2 How It Works

  • Allocation: Rc::new(value) allocates memory on the heap large enough to hold both the value (T) and two reference counts (a “strong” count and a “weak” count, see Section 19.8). It initializes the strong count to 1 and the weak count to 1 (representing the allocation itself), moves value into the allocation, and returns the Rc<T> pointer.
  • Cloning: Calling Rc::clone(&rc_ptr) does not clone the underlying data T. Instead, it creates a new Rc<T> pointer pointing to the same heap allocation and increments the strong reference count. This is a cheap operation (typically just updating the count).
  • Dropping: When an Rc<T> pointer goes out of scope, its destructor decrements the strong reference count.
  • Deallocation: If the strong reference count reaches zero, the heap-allocated data (T) is dropped. If the weak count is also zero at this point (or later becomes zero), the memory for the allocation (which held T and the counts) is deallocated.

Important Constraints:

  • Single-Threaded Only: Rc<T> uses non-atomic reference counting. Sharing or cloning it across threads is not safe and will result in a compile-time error (it does not implement the Send or Sync traits). Use Arc<T> for multi-threaded scenarios.
  • Immutability: Rc<T> only provides shared access, meaning you can only get immutable references (&T) to the contained data. To mutate data shared via Rc<T>, you must combine it with an interior mutability type like RefCell<T> (resulting in Rc<RefCell<T>>).

Example:

use std::rc::Rc;

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

fn main() {
    // 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

  • Runtime Overhead: Incrementing and decrementing the reference count involves a small runtime cost with every clone and drop.
  • No Thread Safety: Restricted to single-threaded use.
  • Reference Cycles: If Rc<T> pointers form a cycle (e.g., A points to B, and B points back to A via Rc), the strong reference count will never reach zero, leading to a memory leak. Weak<T> is needed to break such cycles.