Skip to content
intermediate Phase 11 · Smart Pointers and Interior Mutability

Rc for Shared Ownership

Reference-count a value across multiple owners in single-threaded code.

40m
0 problems
Topic Progress 0%

Several Owners, One Allocation: What the Strong Count Decides

A settings value is needed by a parser, a renderer, and a retry loop. With plain ownership you must pick one winner and hand out &Config references everywhere — workable until someone needs an owner that outlives the function that borrowed it. Rc<T> answers a different question: what if several variables owned the same value, and the value died only when the last owner did?

use std::rc::Rc;

struct Config { endpoint: String, retries: u32 }

let parser = Rc::new(Config { endpoint: String::from("https://api"), retries: 3 });
let renderer = Rc::clone(&parser);      // count 1 -> 2; NO deep copy of Config
let retry    = Rc::clone(&parser);      // count 2 -> 3

assert_eq!(Rc::strong_count(&parser), 3);
assert!(Rc::ptr_eq(&parser, &renderer));   // same allocation, three handles
drop(renderer);                        // 3 -> 2: nothing is freed, no destructor
drop(retry);                           // 2 -> 1
drop(parser);                          // 1 -> 0: NOW Config::drop runs, then dealloc

Rc::clone is a counter increment, not a data copy — that is the entire point, and the reason the API pushes you toward spelling it Rc::clone(&x) instead of x.clone() (clippy's clone_on_ref_ptr lint enforces the distinction, because x.clone() on an Rc<T> is ambiguous to readers: deep or shallow?). Under the hood the allocation carries two counts: the strong count for owners and a weak count for observers. The value's destructor runs when strong hits zero; the allocation itself is reclaimed only when strong and weak are both zero — remember that when we meet Weak.

The count is also a door back to unique access. When you hold the only handle, Rc lets you recover what shared ownership gave up:

let mut cache = Rc::new(vec![1, 2]);
*Rc::get_mut(&mut cache).unwrap() = vec![9];  // Some only while strong == 1 (and weak == 0)
let n = Rc::try_unwrap(cache).unwrap();       // Ok(inner) only while strong == 1

There is also Rc::make_mut, which clones the inner value on write when other handles exist — clone-on-write semantics, the same trick Cow pulls, now driven by the reference count. Everything here costs non-atomic increments and decrements: a plain read-modify-write on a usize in the allocation, which is fast because it happens on one thread.

Where Rc earns its keep is sharing that Box cannot express. Box<dyn EventLoop> has one owner, so it cannot live in two subsystems at once; Box<Config> cloned gives two configs that silently drift apart; but Rc<dyn EventLoop> held by every subsystem is one object with many owners. Typical homes: a parsed AST shared by a debugger and a compiler pass, a Rc<Cache> reachable from every handler of a single-threaded server, a callback list where each subscriber holds the shared service. The cost model is precise: Rc::new(v) makes one allocation containing strong, weak, and the T; size_of::<Rc<T>>() is one pointer, so the counts live with the data and handles stay cheap to pass.

One access rule trips everyone once. Reading through an Rc is free — it derefs to T — but writing is blocked: Rc implements Deref without DerefMut, so *rc = new_value will not compile while shared. Mutation must either happen while you hold the only handle (get_mut, above) or go through types built for it — the subject of Cell, RefCell, and Interior Mutability.

Interview framing: the questions that show up are "what does Rc::clone actually do?", "when does the value get dropped?", and "how do I get a &mut out of an Rc?" — all three are answered by the strong count. The chain of pointers here came from Deref Coercion; Rc derefs to its target exactly as Box does.

Why Rc Refuses to Cross a Thread Boundary

Try to send one to a worker thread and the compiler stops you:

use std::rc::Rc;
use std::thread;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    thread::spawn(move || println!("{:?}", data));
}
error[E0277]: `Rc<Vec<{integer}>>` cannot be sent between threads safely
 --> src/main.rs:5:18
  |
5 |     thread::spawn(move || println!("{:?}", data));
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rc<Vec<{integer}>>` cannot be sent...
  |
note: required because it appears within the closure
note: required by a bound in `std::thread::spawn`

The bounds on thread::spawn are F: FnOnce() -> T + Send + 'static — the closure must be Send, and the closure is Send only if everything it captures is. Rc opts out of the auto traits with explicit negative impls (impl !Send for Rc<T>, impl !Sync for Rc<T>), and that refusal is not conservatism; it is arithmetic. Rc::clone does a non-atomic count += 1. Two threads cloning at once perform a read-modify-write race where one increment vanishes — the classic lost update:

thread A: reads 3          thread B: reads 3
thread A: writes 4         thread B: writes 4   <- one clone never counted
result: strong_count says 4, actual handles: 5  -> freed while in use -> use-after-free

A data race is undefined behavior in Rust's memory model, so the type system refuses to let the possibility exist: since Rc is neither Send nor Sync, any type that contains an Rc inherits the refusal (auto traits propagate through your structs), and the error surfaces at the first thread boundary — thread::spawn, a channel, a Mutex<T> holding it. The same E0277 appears when you push an Rc into an mpsc channel, because Sender::send demands T: Send for the same reason: the value would be produced on one thread and consumed on another.

The fix is Arc — atomic reference count, same API — and the trade is worth quantifying:

Rc<T> Arc<T>
Count update load → add → store (plain usize) atomic read-modify-write (Acquire/Release)
Marker bounds impl !Send, impl !Sync Send + Sync when T: Send + Sync
Cost per clone/drop sub-nanosecond, single core a few ns uncontended; cache-line ping-pong under contention
Correct context ownership shared inside one thread ownership crosses threads

An uncontended atomic increment looks cheap, but N threads cloning one Arc in a hot loop turn the count into a shared cache line that every core must invalidate — clone storms can dominate a profile. That is why the design rule is not "Arc is the safe Rc" but "Rc unless ownership actually crosses a thread": single-threaded programs pay nothing for a guarantee they never use, by using Rc.

Interview framing: "Why is Rc not thread-safe?" is one of the most common Rust interview questions. Answer with the lost-update trace (two readers, two writers, one lost increment), then the consequence (free while in use, UB), then the mechanism (negative impls → E0277 at the boundary), and finish with Arc plus its cost. That fourth beat — naming the cost of atomics — is what distinguishes a prepared answer. The full shared-mutable pattern arrives in Arc and Mutex for Shared State; first, single-threaded sharing needs a mutation story of its own.

Weak: Observing Without Owning

An observer wants to read a value, but it must not decide when that value dies. That is what Weak<T> is for: a pointer that can look but does not own.

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Button { listeners: RefCell<Vec<Weak<Observer>>> }
struct Observer { id: u32 }

impl Observer {
    fn on_click(&self) { println!("observer {} woke", self.id); }
}

impl Button {
    fn subscribe(&self, o: &Rc<Observer>) {
        self.listeners.borrow_mut().push(Rc::downgrade(o));   // weak: count untouched
    }
    fn click(&self) {
        for weak in self.listeners.borrow().iter() {
            match weak.upgrade() {                            // strong handle, temporarily
                Some(obs) => obs.on_click(),                  // alive: upgrade succeeded
                None => println!("dead observer, dropping it"),// already freed
            }
        }
    }
}

let bob = Rc::new(Observer { id: 1 });
let button = Button { listeners: RefCell::new(Vec::new()) };
button.subscribe(&bob);
button.click();        // "observer 1 woke"
drop(bob);             // strong count hits 0 -> Observer dropped NOW
button.click();        // "dead observer, dropping it" — upgrade() returned None

Three properties make this work. Rc::downgrade(&x) returns a Weak without incrementing the strong count, so the observed value's lifetime is unchanged by who watches it. weak.upgrade() returns Option<Rc<T>> — Some with a fresh strong handle while the value lives, None after strong hit zero — so a watcher can never dereference freed data; it must handle the None. And the allocation survives for the weak handles even after the value is destroyed (the value drops at strong == 0; the block goes when weak also reaches 0), which is why upgrade can answer None at all instead of chasing a dangling pointer.

Why not just use Rc for the listeners? Because ownership in both directions is a leak: the button's Rc<Observer> keeps every observer alive forever, and if the observer held Rc<Button> back, neither would ever be freed — reference cycles are the one structural way safe Rust leaks memory, and the whole next topic is about breaking them. The pattern to remember: edges that express "contains / owns" are Rc; edges that express "knows about / points back" are Weak — a parent list owning children with Rc, children pointing at their parent with Weak, is the shape that stays collectable.

The Option return deserves its own note: a raw &T would need a separate liveness check that could dangle the instant after it ran, and a panicking upgrade().unwrap() would turn ordinary cleanup into crashes. Option<Rc<T>> bakes liveness into the value, so the compiler forces the match you saw — you cannot accidentally use a dead observer. Mechanics behind it, for completeness: downgrade bumps only the weak count; upgrade increments strong for the duration of the returned handle (rolling back if strong was already zero); Weak::strong_count reports 0 exactly when upgrade would fail; cloning or dropping a Weak touches nothing about lifetime.

Two practical details from the example. The RefCell around the listener Vec is what lets &self push a subscriber — a plain Vec would need &mut self, which a subscriber holding only &Button never gets; trading compile-time borrow checking for a runtime flag is the next topic's entire subject. And when a structure must begin self-referential — a node needing a Weak to itself before construction finishes — Rc::new_cyclic (stable since Rust 1.60) hands your constructor the Weak first, then completes the Rc.

Interview framing: "how do you avoid a memory leak with Rc?" starts here: Weak::upgrade returns an Option, observers never extend lifetime, and strong/weak counts explain both. Be ready to state the two-phase drop rule — value at strong == 0, allocation at weak == 0 — because it is the detail that separates a memorized answer from understanding.

Quiz

1. Given `let a = Rc::new(String::from("x")); let b = Rc::clone(&a); drop(a);`, what happens?

Question 1 options

2. What does `x.clone()` do when x: Rc<Config>, and which spelling should you prefer?

Question 2 options

3. Why is `Rc<T>` neither Send nor Sync, and what error proves it?

Question 3 options

4. An observer holds `Weak<Config>` and calls upgrade(). What can it observe, and what does it never affect?

Question 4 options

Flashcards

Question

What exactly does Rc::clone do, and when is the value destroyed?

Answer

It increments a non-atomic strong reference count in the shared allocation — no data copy, no allocation. The inner value's destructor runs when the strong count reaches zero (and its block is reclaimed only when the weak count also reaches zero), so intermediate drops free nothing.

Question

How do you get unique access back from an Rc?

Answer

While strong == 1: `Rc::try_unwrap(rc) -> Result<T, Rc<T>>` / `Rc::into_inner` to consume it, `Rc::get_mut(&mut rc) -> Option<&mut T>` for mutation (needs strong == 1 and weak == 0), or `Rc::make_mut` which clones the inner value on write when other handles exist (clone-on-write).

Question

Why can't an Rc cross a thread boundary, and what does the compiler say?

Answer

Its strong/weak counts are non-atomic read-modify-writes; two threads cloning concurrently lose an increment, the value is freed while still in use (data race = UB). Rc opts out with `impl !Send` / `impl !Sync`, so `thread::spawn` reports E0277 "`Rc<T>` cannot be sent between threads safely" — and any struct containing Rc inherits the refusal. Use Arc instead.

Question

What are Weak pointers for, and how does upgrade() behave?

Answer

Non-owning observation: `Rc::downgrade(&x)` creates a Weak without touching the strong count, so watchers never extend lifetime. `weak.upgrade()` returns `Option<Rc<T>>` — Some (temporary strong handle) while the value lives, None once strong hit zero — forcing observers to handle the gone case instead of dereferencing freed memory.

Question

Strong count vs weak count: who frees what, and when?

Answer

Strong count == 0 → the VALUE is dropped (destructor runs). The ALLOCATION is reclaimed only when strong == 0 AND weak == 0. Weak handles therefore keep the block (not the value) alive, which is why upgrade() can answer None safely. Two Rcs pointing at each other keep strong above zero forever — the cycle problem, solved by making one direction Weak.

Revision Notes

Key Takeaways

  • 1. Rc is shared ownership driven by a strong count: clone increments the counter (never the data), drop decrements, and the value dies exactly when strong hits zero
  • 2. The allocation outlives the value only for Weak handles — value freed at strong == 0, block freed when weak also reaches 0
  • 3. Unique access returns on demand at strong == 1 via try_unwrap / get_mut, and make_mut gives clone-on-write through the count
  • 4. Rc is !Send and !Sync because its non-atomic counts race across threads; any containing type inherits that, surfacing as E0277 at the thread boundary (Arc is the atomic twin)
  • 5. Weak pointers observe without owning: upgrade() hands back an Option<Rc<T>>, which is how back-pointers and observers avoid cycles and leaks

Interview Tips

  • • "What does Rc::clone cost?" — one non-atomic increment (plus a decrement on each drop), no copy; say why the API spells it Rc::clone(&x) and mention clippy's clone_on_ref_ptr to show you write it, not just know it
  • • "When is the data actually freed?" — strong == 0 destroys the value; weak == 0 (with strong at zero) reclaims the block; give both halves and you have answered the cycle question before it is asked
  • • "Why can't I send this Rc to another thread?" — walk the lost-update race on the non-atomic counter to use-after-free, quote the E0277 symptom, and name Arc + atomics as the fix (with its cache-line cost)
  • • "Rc vs Arc vs Box in one breath?" — Box: one owner, freed at scope end; Rc: many owners, non-atomic count, single thread only; Arc: many owners, atomic count, cross-thread; then note all three deref to T

Cheat Sheet

Rc for Shared Ownership

let a = Rc::new(v);              // strong = 1
let b = Rc::clone(&a);           // strong = 2  (counter++, NO data copy)
drop(a);                         // strong = 1  (nothing freed)
drop(b);                         // strong = 0  -> T::drop, then (weak==0) free

Counts decide two different things:

Event Effect
Rc::clone strong +1 (non-atomic)
drop of an Rc strong -1; at 0 → value destroyed
Rc::downgrade weak +1 (lifetime unaffected)
last Weak gone (with strong == 0) allocation reclaimed

Unique access is recoverable at strong == 1:

Rc::try_unwrap(rc)        // Result<T, Rc<T>>
Rc::get_mut(&mut rc)      // Option<&mut T>   (strong == 1 && weak == 0)
*Rc::make_mut(&mut rc)    // clone-on-write when shared

Threads: Rc = non-atomic counts → impl !Send / impl !Sync → E0277 at thread::spawn ("cannot be sent between threads safely"); structs containing Rc inherit it. Cross-thread sharing needs Arc (atomic counts, same API).

Observers: Rc::downgrade(&x) -> Weak<T> · weak.upgrade() -> Option<Rc<T>> — never extend lifetime, cannot dangle. Rule: owning edges are Rc, back-pointers are Weak (cycles with two Rcs leak forever).

Continue Learning

Before You Start

Related Reading