Keyboard shortcuts

Press โ† or โ†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

๐Ÿฆ€ Tip of the day 29: Rc<T>, Sharing Without Copying

Tip 18 said every value has exactly one owner. That works for most code, but some patterns genuinely need multiple owners of the same value: graphs where several nodes hold the same child, shared configuration read by many parts of the program, persistent data structures that share most of their content between versions. Rc<T> is the escape valve. It does not bend the XOR borrowing rule from tip 21 (you still only get shared &T out of it); it bends the single-owner rule.

Rc stands for reference counted. It is a smart pointer like Box, but multiple Rc<T> values can point to the same heap allocation. Each clone bumps a count up; each drop bumps it down; when the count reaches zero, the value is freed.

#![allow(unused)]
fn main() {
use std::rc::Rc;

let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);   // b points to the same Vec, count is now 2
let c = Rc::clone(&a);   // c too, count is now 3

println!("count: {}", Rc::strong_count(&a));  // 3
// a, b, c dropped at end of scope, count goes 3 โ†’ 2 โ†’ 1 โ†’ 0, then free
}

Rc::clone(&a) is the idiomatic call. It does not deep-copy the Vec; it just bumps the refcount and hands you another pointer to the same data. You will sometimes see a.clone() instead, which means the same thing for Rc, but the explicit Rc::clone makes the cheapness visible.

One sharp edge: plain assignment (let b = a;) moves the Rc rather than sharing it. a becomes invalid and the refcount stays at 1, because Rc<T> is not Copy. To share, you must call clone explicitly. This is the opposite of C++ shared_ptr (which copies on assignment) and Swift/Java/Python (where assignment just duplicates the reference). It is the same rule as every other non-Copy value in Rust; it just surprises people because the whole point of Rc is sharing.

An Rc<T> derefs to T, so anything you can do with a &T works directly. What you cannot do is mutate the data while it is shared: Rc<T> only hands out shared references, because other Rcs might be aliasing the same value. Two escape hatches exist for when aliasing is not actually happening: Rc::get_mut returns Option<&mut T>, Some only when you are provably the unique owner, and Rc::make_mut does clone-on-write, cloning the inner value first if there are other owners so you always get a &mut T. For genuine shared mutability, combine it with RefCell<T> from tip 30: Rc<RefCell<T>>.

Now the trap with reference counting: cycles. If two values hold Rcs pointing at each other, neither count ever reaches zero, even when nothing else points to them, and the memory leaks. You cannot actually build a cycle with plain Rc alone, because the links are immutable once constructed. Cycles only appear once you combine Rc with RefCell (tip 30) and let the inner value rewrite its own pointers.

The fix when cycles do appear is Weak<T>: a non-owning pointer. A Weak<T> does not contribute to the strong count, so making one direction weak breaks the cycle. You upgrade a Weak<T> back to an Rc<T> with .upgrade(), which returns Option<Rc<T>> because the value may already be gone:

#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};

let weak: Weak<i32> = {
    let strong = Rc::new(42);
    Rc::downgrade(&strong)
};  // strong dropped here, count goes to 0, value freed

assert!(weak.upgrade().is_none());  // gone
}

One important warning: Rc<T> is single-threaded only. The refcount is not atomic, so sharing an Rc across threads would race on the count. The compiler refuses to let you (Rc<T> is neither Send nor Sync). For multi-threaded shared ownership, you want Arc<T> (tip 33). The non-atomic version exists because atomics cost more; for single-threaded code you save the cost.

Comparison:

  • Swift: ARC by language design, deterministic destruction. Cycles need explicit weak.
  • CPython: refcounted with a cycle collector. Prompt destruction in the common case, but it is an implementation detail; PyPy and other Python implementations use tracing GC.
  • Java, C#, JavaScript, Go: tracing GC. No way to opt a specific reference into Rc-style refcounting.
  • C++: std::shared_ptr<T> is the analogue. Atomic refcount by default (heavier than Rustโ€™s Rc, matching Rustโ€™s Arc). C++ weak_ptr plays the role of Weak.
  • Rust: Rc for single-threaded, Arc for cross-thread, opt-in with explicit cost.

The takeaway: Rc<T> lets multiple owners share a heap-allocated value through reference counting. Cheap to clone (one count bump), single-threaded only, watch for cycles and use Weak<T> to break them.

๐Ÿ”‡ Rc is for sharing. To share it, call clone. To move it, do nothing. Pick carefully.