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

  • Creation: Rc::new(value) allocates value on the heap along with a strong reference count, initialized to 1.
  • 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.
  • 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, and the memory 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() {
    let data = Rc::new(SharedData { value: 100 });
    println!("Initial strong count: {}", Rc::strong_count(&data)); // Output: 1

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

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

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

    drop(owner1);
    println!("Count after dropping owner1: {}", Rc::strong_count(&data)); // Output: 2

    drop(owner2);
    println!("Count after dropping owner2: {}", Rc::strong_count(&data)); // Output: 1

    // The original `data` goes out of scope here. Count becomes 0.
    // SharedData is dropped, and memory is freed.
}

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 reference count will never reach zero, leading to a memory leak. Weak<T> is needed to break such cycles.