Smart-Pointer

#161 May 2026

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::cell::RefCell;
use std::rc::Rc;

struct Node {
    name: String,
    parent: RefCell<Option<Rc<Node>>>,    // <-- the trap
    children: RefCell<Vec<Rc<Node>>>,
}

let root  = Rc::new(Node {
    name: "root".into(),
    parent: RefCell::new(None),
    children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
    name: "child".into(),
    parent: RefCell::new(Some(Rc::clone(&root))),  // child owns root
    children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(Rc::clone(&child));  // root owns child

// drop(root); drop(child);  -> strong_count of each is still 1. Memory never freed.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,          // <-- the fix
    children: RefCell<Vec<Rc<Node>>>,
}

let root = Rc::new(Node {
    name: "root".into(),
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});

let child = Rc::new(Node {
    name: "child".into(),
    parent: RefCell::new(Rc::downgrade(&root)),  // weak — doesn't bump strong count
    children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(Rc::clone(&child));

assert_eq!(Rc::strong_count(&root), 1);   // only `root` itself
assert_eq!(Rc::weak_count(&root), 1);     // `child.parent`
assert_eq!(Rc::strong_count(&child), 2);  // `child` + `root.children[0]`

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::rc::{Rc, Weak};

let strong = Rc::new(String::from("alive"));
let weak: Weak<String> = Rc::downgrade(&strong);

if let Some(rc) = weak.upgrade() {
    assert_eq!(*rc, "alive");
}

drop(strong);            // strong count -> 0, value dropped
assert!(weak.upgrade().is_none());

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:

1
2
3
4
use std::rc::Weak;

let dangling: Weak<i32> = Weak::new();
assert!(dangling.upgrade().is_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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::rc::{Rc, Weak};

let a = Rc::new(42);
let w1: Weak<i32> = Rc::downgrade(&a);
let w2: Weak<i32> = Rc::downgrade(&a);

assert_eq!(Rc::strong_count(&a), 1);
assert_eq!(Rc::weak_count(&a), 2);

drop(w1);
assert_eq!(Rc::weak_count(&a), 1);

// Dropping the only strong owner frees the *value* immediately.
// The remaining weak pointer keeps the allocation header alive but
// upgrades will return None.
drop(a);
assert!(w2.upgrade().is_none());

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 with Weak.
  • 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.

159. Rc<T> — Single-Threaded Shared Ownership

The borrow checker’s one-owner rule is a feature until you’re modeling a graph, a cache, or any structure where “who owns this?” honestly has more than one answer. Rc<T> is the escape hatch: a reference-counted pointer that lets multiple owners share the same heap allocation, single-threaded, no locking, no overhead beyond a counter increment.

The pain: a value with no single owner

Imagine a config blob a couple of subsystems need to read. You don’t want to copy it — it’s big — and you can’t give either subsystem a &Config without inventing a parent that outlives them both:

1
2
3
4
5
6
7
8
struct Config { name: String }

fn build() -> (Logger, Metrics) {
    let cfg = Config { name: "app".into() };
    let logger = Logger { cfg: &cfg };  // error: `cfg` does not live long enough
    let metrics = Metrics { cfg: &cfg };
    (logger, metrics)
}

You could clone() the Config and hand each subsystem its own copy. You could thread the lifetime through every type that touches it. Or you could acknowledge that this value genuinely has multiple owners and say so.

Rc<T>: many owners, one allocation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::rc::Rc;

struct Config { name: String }
struct Logger { cfg: Rc<Config> }
struct Metrics { cfg: Rc<Config> }

let cfg = Rc::new(Config { name: "app".into() });
let logger = Logger { cfg: Rc::clone(&cfg) };
let metrics = Metrics { cfg: Rc::clone(&cfg) };

assert_eq!(logger.cfg.name, "app");
assert_eq!(metrics.cfg.name, "app");

Rc::new puts the Config on the heap alongside a strong-count of 1. Rc::clone doesn’t clone the Config — it bumps the counter and hands back another pointer to the same allocation. When a clone is dropped the count decrements; when the last clone is dropped, the Config is destroyed and the heap memory is freed.

The convention is Rc::clone(&x) rather than x.clone(). Both compile, both do the same thing, but the explicit form makes “this is a cheap counter bump, not a deep copy” obvious at the call site — especially helpful when T itself is Clone.

Inspecting the count

Rc::strong_count is a debugging window into the same counter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::rc::Rc;

let a = Rc::new(String::from("shared"));
assert_eq!(Rc::strong_count(&a), 1);

let b = Rc::clone(&a);
let c = Rc::clone(&a);
assert_eq!(Rc::strong_count(&a), 3);

drop(b);
assert_eq!(Rc::strong_count(&a), 2);

You almost never need to read it in production code — its main use is asserting “yes, this is actually shared” in tests, or understanding a memory leak.

You only get &T through an Rc<T>

The price for shared ownership is read-only access. Rc<T> is Deref<Target = T>, but not DerefMut. Try to mutate and the compiler stops you:

1
2
3
4
5
6
use std::rc::Rc;

let cfg = Rc::new(String::from("app"));
let other = Rc::clone(&cfg);
cfg.push_str("-prod");
// error: cannot borrow data in an `Rc` as mutable

This is the correct default. If two owners could both call &mut simultaneously, you’d have a data race in single-threaded code — exactly the bug Rust exists to prevent. So you need a runtime borrow check, which is what RefCell<T> provides.

The classic pattern: Rc<RefCell<T>>

When the shared value also needs to mutate, wrap it in a RefCell. The outer Rc gives you shared ownership; the inner RefCell gives you borrow_mut through &self:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::rc::Rc;
use std::cell::RefCell;

let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));

let writer_a = Rc::clone(&log);
let writer_b = Rc::clone(&log);

writer_a.borrow_mut().push("a says hi".into());
writer_b.borrow_mut().push("b says hi".into());

let snapshot = log.borrow();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot[0], "a says hi");

Every Rc::clone owns the cell; every borrow goes through RefCell’s runtime check. Forget the inner RefCell and you’ll be staring at cannot borrow as mutable errors. Forget the outer Rc and you can’t share the cell in the first place. The pair shows up so often that “Rc<RefCell<…>>” should land in your fingers as one token.

Rc::clone vs (*rc).clone()

It’s worth seeing the difference once:

1
2
3
4
5
6
7
8
9
use std::rc::Rc;

let a = Rc::new(vec![1, 2, 3]);

let cheap = Rc::clone(&a);        // counter += 1, no allocation
let expensive: Vec<i32> = (*a).clone();   // brand new Vec, separate heap allocation

assert_eq!(Rc::strong_count(&a), 2);   // a + cheap; `expensive` isn't an Rc at all
assert_eq!(expensive, vec![1, 2, 3]);

Rc<T> doesn’t change what T::clone does — it just adds a much cheaper way to make another pointer to the same T. Use Rc::clone for the pointer; reach for (*rc).clone() only when you really do want a fresh, independent T.

The catch: Rc<T> is not Send

Rc is deliberately not thread-safe. The counter increment isn’t atomic; if two threads bumped it simultaneously you could undercount and free a still-referenced allocation. Try to move one across a thread boundary and the compiler refuses:

1
2
3
4
5
6
use std::rc::Rc;
use std::thread;

let a = Rc::new(42);
thread::spawn(move || println!("{a}"));
// error: `Rc<i32>` cannot be sent between threads safely

That refusal is the whole reason Rc is cheap — no atomic ops, no fences. Cross threads and you want the atomically-counted sibling: Arc<T>, which this afternoon’s bite is about. Same API, same shape, just Send + Sync and a slightly costlier clone.

The other catch: reference cycles

Two Rcs pointing at each other never drop, because each is keeping the other’s count above zero:

1
2
3
4
struct Node {
    next: RefCell<Option<Rc<Node>>>,
}
// build a -> b -> a … and neither will ever be freed

Rc is a counter, not a tracing GC — it can’t notice that an island of nodes is unreachable from the outside as long as the nodes are still pointing at each other. The fix is Weak<T>, the non-owning sibling pointer you get from Rc::downgrade. That’s tomorrow’s bite. For now: if your data is a tree, plain Rc<RefCell<T>> is fine; if it’s a graph or has back-pointers, plan for Weak.