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)
allocatesvalue
on the heap along with a strong reference count, initialized to 1. - 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. - 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 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() { 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 viaRc
), the reference count will never reach zero, leading to a memory leak.Weak<T>
is needed to break such cycles.