7.2 · Rc<T> and Arc<T>: Reference Counting Strategies
Domain 7 — Smart Pointers and Heap Allocation Duration: ~15 minutes Library components:
std::rc::Rc,std::rc::Weak,std::sync::Arc,std::sync::Weak
Introduction
Box<T> (tutorial 7.1) gives a value exactly one owner. But some data has several owners. Examples are a configuration that each subsystem shares, and a node that more than one parent can reach. You cannot predict which of the owners lives longest.
Reference counting is the solution. The value is on the heap, adjacent to a counter. Each new handle increments the counter. When the last handle drops, the value drops too.
The standard library has two reference-counted pointers. They differ in exactly one property:
Rc<T>has plain integer counters. It is fast, and it is for one thread only (!Send).Arc<T>has atomic counters. It has a slightly higher cost, and it is safe to share across threads.
This tutorial shows:
Rc::cloneandstrong_count.- How to take the value back with
try_unwrap. - How to break cycles with
Weak<T>. - Self-referential construction with
Rc::new_cyclic. - Clone-on-write mutation with
make_mut. - Why the standard library has both
RcandArc.
Rc<T>: Shared Ownership
An Rc<T> handle points to one heap block that holds three items: the strong count, the weak count, and the value. Rc::clone copies the pointer and increments the strong count. It does not touch the value. When the strong count becomes zero, Rc drops the value.
Figure: One Allocation, Many Handles — Rc Heap Layout
use std::rc::Rc;
// Config is a struct with two fields: theme (String) and retries (u32).
let config = Rc::new(Config { theme: String::from("dark"), retries: 3 }); // Rc<Config>
assert_eq!(Rc::strong_count(&config), 1);
// Logger and Fetcher are structs with one field each: config: Rc<Config>.
let logger = Logger { config: Rc::clone(&config) }; // count: 1 → 2
let fetcher = Fetcher { config: Rc::clone(&config) }; // count: 2 → 3
assert_eq!(Rc::strong_count(&config), 3);
assert!(Rc::ptr_eq(&config, &logger.config)); // same allocation
assert_eq!(logger.config.retries, 3); // Deref, like Box
Remember these two rules:
- Write
Rc::clone(&x), notx.clone(). The associated-function syntax tells the reader that the call is a cheap copy of the handle, not a deep clone. The same convention applies toRc::strong_count,Rc::ptr_eq, and similar functions. The standard library defines them as associated functions, so that they can never conflict with methods ofT. Rchas noDerefMut. Shared data is immutable throughRc.logger.config.retries = 5fails witherror[E0594]: cannot assign to data in an Rc. The two alternatives areRc::make_mut(slide 6) andRefCell(tutorial 7.3).
Reclaiming the Value: try_unwrap
Shared ownership does not have to be permanent. Rc::try_unwrap(rc) moves the value out if the strong count is exactly 1. If not, it returns the Rc in the Err variant, so you lose nothing:
// `config`, `logger`, and `fetcher` are from the previous snippet. The count is 3.
drop(logger); // count: 3 → 2
// try_unwrap fails and returns the same Rc in Err. The pattern binds it to `config` again.
let Err(config) = Rc::try_unwrap(config) else { // fetcher still shares it
unreachable!()
};
drop(fetcher); // count: 2 → 1
// The count is 1, so try_unwrap returns Ok(Config), and expect gives the Config.
let owned: Config = Rc::try_unwrap(config).expect("last handle standing");
The successful unwrap costs no clone. try_unwrap moves the Config out and deallocates the counter block. Sometimes you want a move when the handle is unique and a clone when it is not. Rc::unwrap_or_clone does exactly that in one call.
07_06_rc_shared_ownership.rs prints:
created: strong_count = 1
+ logger, + fetcher: strong_count = 3
- logger: strong_count = 2
try_unwrap while shared: Err (count still 2)
try_unwrap as sole owner: Ok -> Config { theme: "dark", retries: 3 }
All assertions passed.
Weak<T>: Breaking Cycles
Reference counting has one known weakness: cycles. If A holds an Rc to B and B holds an Rc to A, their counts can never become zero. Neither destructor can run. The borrow checker cannot help, because nothing is wrong with the borrows. The usual example is a parent↔child tree: parents own their children, and children point back to their parents.
Weak<T> is the solution. It is a non-owning handle that does not keep the value alive. Rc counts the weak handles separately (weak_count). You cannot dereference a Weak. Instead, you call upgrade(), which returns Option<Rc<T>>: Some while the value is alive, and None after the value drops. Thus the design itself prevents dangling pointers and use-after-free.
Figure: Ownership Down, Weak Up — the Cycle Broken
use std::cell::RefCell;
use std::rc::{Rc, Weak};
// RefCell (tutorial 7.3) lets the code change the links through a shared Rc.
struct TreeNode {
parent: RefCell<Weak<TreeNode>>, // non-owning back-pointer
children: RefCell<Vec<Rc<TreeNode>>>, // owning forward pointers
}
// `/* … */` replaces `children: RefCell::new(Vec::new())`.
// Weak::new() is an empty Weak: the root has no parent.
let root = Rc::new(TreeNode { parent: RefCell::new(Weak::new()), /* … */ });
// Rc::downgrade makes a Weak from an Rc. It increments only the weak count.
let leaf = Rc::new(TreeNode { parent: RefCell::new(Rc::downgrade(&root)), /* … */ });
root.children.borrow_mut().push(Rc::clone(&leaf)); // the root owns the leaf
assert_eq!(Rc::strong_count(&root), 1); // the Weak does not keep root alive
assert_eq!(Rc::weak_count(&root), 1); // the Weak in leaf.parent
// The root is alive, so upgrade returns Some(Rc<TreeNode>).
let parent = leaf.parent.borrow().upgrade().expect("parent alive");
The design rule: ownership goes in one direction, and each back-edge is Weak. Example 07_07_rc_weak_cycles.rs uses a drop counter to make the difference visible. The version with the strong cycle never runs a destructor. The version with Weak drops the two nodes when the scope ends. The example prints:
strong cycle built: root count = 2, leaf count = 2
scope ended — destructors run: 0 (leaked!)
weak design: root strong = 1, weak = 1; leaf strong = 2
scope ended — destructors run: 2 (no leak)
observer.upgrade() after death: None
All assertions passed.
Rc::new_cyclic: Self-Referential Construction
Sometimes a value needs a Weak reference to itself. Examples are a job that gives other code handles to itself, and a node that can give its own address to registries. This is a circular problem. The Weak comes from Rc::downgrade, but the Rc exists only after you build the value.
Rc::new_cyclic solves the problem in one call:
- It allocates the heap block.
- It passes a
Weakto your constructor closure. The value is not alive at this time. - It moves the value that the closure returns into the allocation.
struct Job { id: u32, me: Weak<Job> } // `me` is the weak self-reference
impl Job {
fn new(id: u32) -> Rc<Self> {
// `me` is a &Weak<Job> that points to the new allocation.
Rc::new_cyclic(|me| {
assert!(me.upgrade().is_none()); // the Job is not constructed yet
Job { id, me: me.clone() } // keep a copy of the Weak in the Job
})
}
// Gives a non-owning handle to this job.
fn handle(&self) -> Weak<Job> { self.me.clone() }
}
let job = Job::new(7); // Rc<Job>
assert_eq!(Rc::strong_count(&job), 1);
assert_eq!(Rc::weak_count(&job), 1); // the self-reference
Inside the closure, upgrade() returns None. This guarantee makes the API sound, because you cannot read a value that is not complete. The self-reference is Weak for the same reason as the back-pointers. A strong Rc<Self> field would be a cycle of one node that nothing can break.
Note what occurs at the end. When the last strong handle drops, Rc destroys the value immediately. But the counter block stays allocated until the last Weak drops. upgrade() reads that counter block.
07_08_rc_new_cyclic.rs prints:
job 7 created: strong = 1, weak = 1
two handles out: strong = 1, weak = 3
job dropped: handles now upgrade to None
All assertions passed.
make_mut: Clone-on-Write
Rc::make_mut(&mut rc) returns &mut T, and first it makes sure that the mutation is safe. It is the copy-on-write operation for shared ownership (compare Cow<'_, T>, tutorial 1.2). Its behavior depends on the other handles to the allocation:
| State of the allocation | What make_mut does |
|---|---|
| One strong owner, no weak handles | Mutates in place, with no copy |
| Other strong handles exist | Clones the value. Your handle then points to the copy. |
| Only weak handles exist | Moves the value to a new allocation. It disassociates the weak handles. |
// doc: Rc<Vec<String>>, a document of two lines
let mut doc = Rc::new(vec![String::from("# Notes"), String::from("- buy coffee")]);
let snapshot = Rc::clone(&doc); // cheap snapshot: the strong count is 2
// `doc` is shared, so make_mut clones the Vec first. The push goes to the copy.
Rc::make_mut(&mut doc).push(String::from("- water plants"));
assert_eq!(Rc::strong_count(&doc), 1); // each handle is now the only owner
assert_eq!(Rc::strong_count(&snapshot), 1); // of its own allocation
assert!(!Rc::ptr_eq(&doc, &snapshot)); // two different allocations
assert_eq!(snapshot.len(), 2); // the snapshot did not change
assert_eq!(doc.len(), 3); // `doc` has the new line
A snapshot is O(1). You pay for a real copy only at the first write that another handle could observe. The rule for weak handles is less obvious, but it protects in the same way. After make_mut, the old Weak handles upgrade to None, and they do not see your mutation.
07_09_rc_make_mut_cow.rs prints:
before edit: strong_count = 2, shared = true
after edit: doc has 3 lines, snapshot still 2 (diverged = true)
sole-owner edit: in place (allocation unchanged = true)
weak-only edit: weak refs disassociated (upgrade -> None)
unwrap_or_clone: recovered Vec with 5 lines
All assertions passed.
Arc<T>: Crossing Threads — and Why Rc Cannot
The counters of Rc are plain integers. If two threads clone at the same time, each could read 2 and each could write 3. Then the count misses one reference, and eventually a use-after-free occurs. Rust does not rely on you to prevent this. The type system makes it impossible: Rc<T> is !Send, so code that gives an Rc to a thread does not compile:
// This code does not compile.
let rc = std::rc::Rc::new(vec![1, 2, 3]);
thread::spawn(move || rc.len()); // the closure moves `rc` to the new thread
// error[E0277]: `Rc<Vec<i32>>` cannot be sent between threads safely
Arc<T> (atomically reference counted) replaces the plain counters with atomic operations. That is the only difference, and the only cost. The API is almost the same as the API of Rc, method for method: Arc::clone, strong_count, try_unwrap, make_mut, Arc::downgrade to std::sync::Weak, and Arc::new_cyclic.
use std::sync::Arc;
use std::thread;
let readings: Arc<Vec<i64>> = Arc::new((1..=1000).collect()); // the values 1 to 1000
let mut handles = Vec::new(); // one JoinHandle<i64> for each worker
for worker in 0..4 {
let data = Arc::clone(&readings); // atomic increment of the count
// `move` gives the clone to the thread. The thread drops it when the closure ends.
handles.push(thread::spawn(move || {
// Each worker adds its own quarter of the data: 250 elements.
data[worker * 250..(worker + 1) * 250].iter().sum::<i64>()
}));
}
let total: i64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
assert_eq!(total, 500_500); // the sum of 1..=1000
assert_eq!(Arc::strong_count(&readings), 1); // deterministic: join waited for each worker
let owned = Arc::try_unwrap(readings).expect("sole owner"); // Vec<i64>, moved out
Arc alone gives shared read access. For shared mutation, use it together with the synchronization types: Arc<Mutex<T>>, Arc<RwLock<T>> (tutorial 6.2), or atomics (6.3). Select Rc when the sharing provably stays on one thread. The compiler tells you as soon as that is no longer true.
07_10_arc_threads.rs prints:
sum computed by 4 workers: 500500
after joins: strong_count = 1
reclaimed the Vec by value: len = 1000
sync::Weak after drop: upgrade -> None
All assertions passed.
Summary
| Concept | Key point |
|---|---|
Rc<T> | Shared ownership with plain counters, for one thread only (!Send) |
Arc<T> | The same API with atomic counters. It is the thread-safe type. |
Rc::clone(&x) | Copies the handle and increments strong_count. It does not touch the value. |
Rc::ptr_eq | Tells you if two handles share one allocation |
No DerefMut | Shared data is immutable. To mutate, use make_mut or RefCell (7.3). |
Rc::try_unwrap | Moves the value out when the count is 1. If not, Err(rc) returns the handle. |
Rc::unwrap_or_clone | Moves the value if the handle is unique, and clones it if not |
Weak<T> | Non-owning handle with a separate count. upgrade() returns Option<Rc<T>>. |
| Cycle rule | Ownership goes in one direction, and each back-edge is Weak |
Rc::new_cyclic | Builds a value that holds a Weak to itself. upgrade returns None during construction. |
Rc::make_mut | Clone-on-write &mut T: in place if unique, clone if shared. It disassociates weak handles. |
| Value vs. allocation | The value drops when the strong count is 0. The counter block stays until the weak count is also 0. |
Code Examples
| File | Description |
|---|---|
07_06_rc_shared_ownership.rs | Rc::new/clone, strong_count, ptr_eq, try_unwrap with a shared config |
07_07_rc_weak_cycles.rs | Parent↔child tree: a strong cycle that leaks, and the same tree with Weak back-pointers and no leak |
07_08_rc_new_cyclic.rs | Rc::new_cyclic for a job that gives other code Weak handles to itself |
07_09_rc_make_mut_cow.rs | make_mut clone-on-write: the shared, unique, and weak-only cases, and unwrap_or_clone |
07_10_arc_threads.rs | Arc fan-out to worker threads, why Rc is !Send, Arc::try_unwrap, sync::Weak |