161. Weak<T> — The Non-Owning Pointer That Breaks Rc Cycles
Rc<T> is a counter, not a tracing GC: two Rcs pointing at each other will sit in memory forever, each propping the other’s strong-count above zero. Weak<T> is the cure — a pointer that observes an Rc’s allocation without keeping it alive.
The leak Rc lets you write
A parent node owning its children with Rc, and each child holding an Rc back at the parent, looks innocent — and leaks every node:
| |
Once root and child go out of scope, their last external Rcs drop, but each still holds an Rc to the other. The strong-counts never hit zero. Drop never runs. The allocation stays put until the process exits.
Weak<T>: a pointer that doesn’t keep things alive
| |
Strong pointers own; weak pointers observe. Rc::downgrade(&root) gives you a Weak<Node> that points at the same allocation but only bumps the weak counter. The strong counter — the one that decides when to drop — is unaffected. When the last Rc to a node disappears, the node is destroyed, even if a hundred Weaks are still aimed at it.
Reading through a Weak: upgrade
A Weak doesn’t deref. To get at the value, ask for a temporary Rc back — and be ready for None, because the allocation may already be gone:
| |
upgrade is the only way to read through a Weak<T>. The Option return type is the whole point: it forces every caller to handle the case where the upgraded pointer no longer refers to anything. That’s exactly the discipline a back-pointer in a tree needs — “give me my parent, if it’s still around.”
A standalone Weak: Weak::new
You usually want a Weak field initialised before its target exists. Weak::new makes one that points at nothing and upgrades to None:
| |
This is what filled the parent field of root above before we had any pointer to give it. No allocation happens until something is actually downgraded into it.
Counts: strong_count and weak_count
Both counters are inspectable. weak_count is what Rc::downgrade increments; strong_count is the one that gates the drop:
| |
One nuance worth knowing: a live Weak keeps a small allocation header around (so it can check whether the value is still there), but not the value itself. Drop runs on the inner T as soon as the strong-count hits zero, even if a million Weaks outlive it.
When to reach for Weak
Whenever the ownership graph has a back-edge or a cycle:
- Parent pointers in a tree. Children own children with
Rc; children point back to parents withWeak. - Observer / listener lists. A subject holds
Weak<Listener>so listeners can be dropped externally without first deregistering. - Caches. A cache holding
Weak<T>lets entries vanish the moment their last real user lets go.
The rule is mechanical: pick one direction in the cycle to be the owner (Rc), and make every edge that closes the loop a Weak. If that direction is hard to choose, the lifetime question is probably a real design question hiding inside the data.
Arc<T> has the same thing
Arc<T> gives you the same pair on the thread-safe side: Arc::downgrade returns a std::sync::Weak<T> (different type, same shape) that you upgrade to an Option<Arc<T>>. Same rules, same idiom — atomic counters under the hood.
Reach for Weak the moment any node in your structure needs to point at something that also points back. The borrow checker can’t catch this leak; making the back-edge weak is the design that does.