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 thevalue
(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), movesvalue
into the allocation, and returns theRc<T>
pointer. - Cloning: Calling
Rc::clone(&rc_ptr)
does not clone the underlying dataT
. Instead, it creates a newRc<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 heldT
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 theSend
orSync
traits). UseArc<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 viaRc<T>
, you must combine it with an interior mutability type likeRefCell<T>
(resulting inRc<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 viaRc
), the strong reference count will never reach zero, leading to a memory leak.Weak<T>
is needed to break such cycles.