Using Rc<T> to Share Data

Understand how Rust's Rc smart pointer enables multiple ownership for immutable data in single-threaded programs, and learn to share data safely with reference counting.

Rust’s ownership model gives every value a single owner at a time, and the compiler enforces that rule ruthlessly. For most programs, that clarity is exactly what you want. But some data structures simply do not fit the “one owner, one lifetime” mold. A node in a graph is conceptually owned by every edge that points to it. A shared configuration object might need to outlive any single function that reads it, without the compiler being able to prove which one expires last. Rc<T> — short for reference counted — is Rust’s answer to those scenarios.

Rc<T> is a smart pointer that allocates a value on the heap and keeps a counter tracking how many Rc<T> instances point to that same value. When you clone an Rc<T>, the counter increases; when an Rc<T> goes out of scope, the counter decreases. The heap allocation is freed only when the counter hits zero, meaning no owner is left. This gives you multiple ownership while staying inside Rust’s safety guarantees — as long as you stay in a single thread.

Mental Model:

Think of Rc<T> as a television in a shared living room. The first person who walks in turns it on. Others can join and watch. Only when the last person leaves does someone turn the TV off. Nobody turns it off while someone is still watching — that would be a disaster, and Rc<T> prevents exactly that disaster for heap data.

Why a Single Owner Isn’t Always Enough

Standard references (&T, &mut T) in Rust are borrows: they let you temporarily inspect or mutate data without taking ownership. That works beautifully when there is a clear, single owner that outlives all borrowers. But what if you can’t name such an owner at compile time? Consider a cons list — a recursive data structure borrowed from Lisp — where two different lists want to share the same tail.

A cons list is either a value paired with another list (the “cons” cell) or a terminator (Nil). If you define it using Box<T> for heap allocation, the tail is owned by the box. That means you cannot have two lists that both point to the same tail — you would be trying to move the same value into two different owners.

src/main.rs
enum List {
    Cons(i32, Box<List>),
    Nil,
}
use List::{Cons, Nil};
fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));   // `a` is moved into `b`
    let c = Cons(4, Box::new(a));   // ERROR: use of moved value
}

The compiler refuses this code outright:

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                               - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

The move is visible in the error: a has been transferred into b, so c cannot touch it. You could try to use references and lifetime annotations, but that forces all list elements to live exactly as long as the entire list — a constraint that breaks down the moment any temporary value appears.

The Lifetime Escape Route Is Fragile:

Changing Cons to hold &List instead of Box<List> forces you to annotate lifetimes and, crucially, prevents you from ever having a temporary Nil that is borrowed before it is dropped. The borrow checker rightly rejects let a = Cons(10, &Nil); because the temporary Nil would be destroyed before a could hold a valid reference to it. This is not a bug — it’s Rust protecting you from dangling pointers.

Sharing Ownership with Rc<T>

Instead of a Box, you can wrap the tail in Rc<T>. Now a cons cell doesn’t own its tail exclusively; it shares ownership through a reference count. The definition becomes:

src/main.rs
use std::rc::Rc;
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

When you create the shared tail, you place it inside an Rc. Then, every list that wants to use that tail clones the Rc — not the data, just the smart pointer that owns the counter.

src/main.rs
use std::rc::Rc;
use List::{Cons, Nil};
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

After this code runs, both b and c point to the same heap‑allocated List that starts with 5 followed by 10 and Nil. There are now three Rc handles alive: a, the one inside b’s Cons, and the one inside c’s Cons. The reference count is 3.

Rc::clone does not deep‑copy the list nodes. It only increments an integer that lives alongside the heap allocation. That is why the Rust community prefers the explicit Rc::clone(&a) syntax over a.clone() — it signals to anyone reading the code that this is a lightweight reference‑count bump, not an expensive data duplication.

Compilation Success:

If the snippet above compiles without errors, you’ve correctly replaced exclusive ownership with shared ownership. The compiler no longer complains about a moved value because a was never moved — only its reference count was incremented.

Watching the Reference Count in Action

The Rc type exposes a function Rc::strong_count that lets you inspect the current number of strong references. You can use it to see the count rise and fall as clones are created and destroyed.

src/main.rs
use std::rc::Rc;
use List::{Cons, Nil};
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Running this program prints:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

When c is created inside the inner block, the count jumps to 3. As soon as that block ends, c is dropped, and the count falls back to 2 automatically. You never call a manual “decrement” function — the Drop trait implementation for Rc<T> handles that. When b and eventually a go out of scope at the end of main, the count reaches zero and the entire chain of cons cells is freed.

The data remains alive exactly as long as it is needed, and not a moment longer. That’s the core promise of reference counting.

What Rc<T> Gives You — and What It Doesn’t

Rc<T> solves the multiple‑ownership problem cleanly, but it comes with constraints that matter when you design larger systems.

Immutable Access Only

You cannot get a mutable reference to the data inside an Rc<T> through the pointer itself. Rc<T> implements Deref, giving you &T, but not DerefMut. This prevents you from accidentally creating mutable aliases, which would break Rust’s compile‑time guarantees. If you need to mutate data that is shared via Rc, you’ll combine it with RefCell<T> or Mutex<T> — topics explored later in this chapter.

Rc Does Not Enable Interior Mutability:

Beginners sometimes expect that cloning an Rc creates an independent mutable copy of the data. It does not. All clones point to the same heap value. If you need shared mutation, you must wrap the inner type in a RefCell — something covered in the next major section.

Strictly Single‑Threaded

Rc<T> uses non‑atomic reference counting for performance, which makes it !Send and !Sync. The compiler will stop you from moving an Rc<T> to another thread. This is a compile‑time safety net, not a runtime footgun.

Thread Safety Is Not Negotiable:

Attempting to share an Rc<T> across threads results in a compilation error like the trait 'Send' is not implemented for 'Rc<T>'. If you ever encounter code that circumvents this with unsafe, it can lead to data races and undefined behavior. For multi‑threaded shared ownership, use Arc<T> — the atomic reference‑counted sibling of Rc.

Reference Cycles Can Leak Memory

If two Rc<T> values point at each other (directly or through a chain), their reference counts never drop to zero. Rust’s memory safety guarantees do not cover this case; the standard library does not automatically detect and break cycles. You must structure your data to avoid cycles, or use Weak<T> to break them.

This is a real‑world hazard, especially in graph or tree‑like structures where nodes hold references to parents and children. The next several sections explore how Weak<T> helps you prevent memory leaks.

Where Rc<T> Belongs in Real Code

You reach for Rc<T> when you have data that several parts of a single‑threaded program need to hold onto independently, and none of them is clearly the “last one standing.” Common examples include:

  • Compiler intermediate representations: an abstract syntax tree (AST) where many analysis passes hold pointers to shared sub‑trees.
  • GUI frameworks: multiple widgets referencing the same underlying configuration object or style data.
  • Graphs and linked structures: nodes shared among multiple edges or lists, as seen in the cons list example.
  • Immutable persistent data structures: where “modification” is done by creating a new version that shares most of its structure with the old one.

The cons list you worked with is the canonical teaching example, but the pattern generalises to any situation where compile‑time ownership is too rigid and you are willing to trade a small runtime counter overhead for flexibility.