Quiz
1. Given `let a = Rc::new(String::from("x")); let b = Rc::clone(&a); drop(a);`, what happens?
2. What does `x.clone()` do when x: Rc<Config>, and which spelling should you prefer?
3. Why is `Rc<T>` neither Send nor Sync, and what error proves it?
4. An observer holds `Weak<Config>` and calls upgrade(). What can it observe, and what does it never affect?
Flashcards
Question
What exactly does Rc::clone do, and when is the value destroyed?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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).