Rc<T>
std::rc::Rc<T> (Reference Counted) is a single-threaded smart pointer that enables multiple owners of the same heap-allocated value T. It tracks the number of active owners at runtime and drops the value and its heap allocation only when the last Rc<T> pointing to it is dropped.
Memory Layout of Rc<T>
Calling Rc::new(value) performs a single heap allocation containing an internal RcBox<T> structure with two usize reference counts (strong and weak) followed immediately by the payload T. Each Rc<T> handle on the stack is a single thin pointer (8 bytes on 64-bit targets) pointing to the heap RcBox<T>.
Stack Handles Single Heap Allocation (RcBox<T>)
+---------------+ +-------------------------+
| rc1: Rc<T> ---+---------------> | strong: Cell<usize> (2) |
+---------------+ +-------> | weak: Cell<usize> (1) |
| | value: T |
+---------------+ | +-------------------------+
| rc2: Rc<T> ---+-------+
+---------------+
Cloning an Rc<T> vs. Cloning T
Cloning an Rc<T> increments the strong reference count by 1 and returns a new 8-byte pointer to the same heap allocation; it never copies or clones the underlying T. By convention, Rc::clone(&rc) is preferred over rc.clone() so readers can immediately see that only a cheap pointer-and-counter increment is taking place.
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("shared config"));
println!("initial strong count: {}", Rc::strong_count(&a));
let b = Rc::clone(&a);
println!("after cloning b: {}", Rc::strong_count(&a));
{
let c = Rc::clone(&a);
println!("inside inner scope: {}", Rc::strong_count(&c));
println!("value via c: {}", c);
}
println!("after c is dropped: {}", Rc::strong_count(&a));
println!("b and a same addr: {}", Rc::ptr_eq(&a, &b));
}
initial strong count: 1
after cloning b: 2
inside inner scope: 3
value via c: shared config
after c is dropped: 2
b and a same addr: true
Essential Rc<T> Associated Functions
| Function | Signature | Behavior |
|---|---|---|
Rc::new(value) |
fn new(value: T) -> Rc<T> |
Allocates RcBox<T> on the heap with strong = 1 |
Rc::clone(&this) |
fn clone(this: &Rc<T>) -> Rc<T> |
Increments strong count and returns a new handle |
Rc::strong_count(&this) |
fn strong_count(this: &Rc<T>) -> usize |
Returns current number of owning Rc<T> handles |
Rc::weak_count(&this) |
fn weak_count(this: &Rc<T>) -> usize |
Returns current number of active Weak<T> handles |
Rc::downgrade(&this) |
fn downgrade(this: &Rc<T>) -> Weak<T> |
Creates a non-owning Weak<T> pointer |
Rc::get_mut(&mut this) |
fn get_mut(this: &mut Rc<T>) -> Option<&mut T> |
Returns Some(&mut T) only if strong == 1 and weak == 0 |
Rc::make_mut(&mut this) |
fn make_mut(this: &mut Rc<T>) -> &mut T |
Returns &mut T, cloning T into a fresh Rc if shared (copy-on-write) |
Rc::try_unwrap(this) |
fn try_unwrap(this: Rc<T>) -> Result<T, Rc<T>> |
Moves T out of Rc<T> if strong == 1; otherwise returns Err(this) |
Copy-on-Write Mutation with Rc::make_mut and Rc::get_mut
Because Rc<T> can have multiple readers at once, Deref only yields an immutable reference &T. However, when an Rc<T> is the sole owner (strong_count == 1 and weak_count == 0), Rc::get_mut grants direct &mut T access in place, and Rc::make_mut automatically clones the inner T on write when shared by multiple owners:
use std::rc::Rc;
fn main() {
let mut primary = Rc::new(vec![1, 2, 3]);
// Sole owner: `get_mut` mutates in place without cloning the Vec
if let Some(vec_mut) = Rc::get_mut(&mut primary) {
vec_mut.push(4);
}
println!("primary (unique): {:?}", primary);
// Create a second owner:
let snapshot = Rc::clone(&primary);
// Shared owner: `make_mut` clones the Vec into a new allocation for `primary`
Rc::make_mut(&mut primary).push(5);
println!("snapshot (unchanged): {:?}", snapshot);
println!("primary (branched): {:?}", primary);
}
primary (unique): [1, 2, 3, 4]
snapshot (unchanged): [1, 2, 3, 4]
primary (branched): [1, 2, 3, 4, 5]
Why Rc<T> Cannot Cross Threads (!Send and !Sync)
Rc<T> stores its reference counts in non-atomic Cell<usize> fields so increments and decrements compile to ordinary CPU integer instructions without atomic synchronization overhead. Because concurrent non-atomic increments from two threads would cause a data race and corrupt the reference count, Rc<T> implements neither Send nor Sync.