Smart-Pointer

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.