Rc<T>, the Reference Counted Smart Pointer

Understanding shared ownership in Rust through Rc<T>, how reference counting works, and when to use it in single-threaded scenarios

Rust’s ownership system usually gives every piece of data exactly one owner at a time. That works well for most programs — you know who’s responsible for cleaning up a value, and the compiler verifies it for you. But some real-world data structures have multiple legitimate owners. In a graph, every edge that points to a node holds a stake in that node’s lifetime. If you tried to model that with plain ownership, you’d hit a wall: you cannot have two variables that both own the same node.

Rc<T> exists for exactly that situation. Its name is short for reference counted. It is a smart pointer that sits on the heap, keeps a tally of how many places in your code are currently pointing to it, and only drops the inner data when that tally hits zero. You can think of it as shared ownership with a head count — as long as someone is still using the value, it stays alive.

A helpful analogy is a TV in a common room. The first person who comes in turns it on. Others join later and watch together. Nobody turns it off while someone else is still watching. When the last person leaves, the TV gets turned off. Rc<T> works the same way: the first Rc creates the value, subsequent clones increase the viewer count, and the value is cleaned up automatically when the count falls back to zero.

Rc<T> is strictly single-threaded. The counter is not atomic; sharing an Rc across threads would be a data race. When you need reference counting across threads, Rust provides Arc<T>, which we’ll cover later in the concurrency chapter. For now, everything you read here applies only to code that runs on one thread at a time.

Importing Rc:

Rc lives in std::rc and is not in the prelude. Add use std::rc::Rc; to the top of your file to use it.

Using Rc<T> to Share Data

The clearest way to see why Rc<T> is necessary is to try sharing a value without it — and watch the compiler stop you. We’ll use a classic functional data structure, the cons list, which is a recursive linked list built from nested pairs.

Start with a definition that uses Box<T>, a single-owner smart pointer:

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));
    let c = Cons(4, Box::new(a)); // error: use of moved value: `a`
}

a represents the list (5, (10, Nil)). We want b to be (3, a) and c to be (4, a), so both lists share the same tail. But Box enforces single ownership. When b is constructed, a is moved into the Box inside b; a no longer exists. Trying to use it again for c fails with use of moved value.

This is not a missing feature — it’s Rust’s ownership model refusing to let two variables own the same memory. The error makes sense because, without some form of sharing, there’s no way to know who should free the list.

Rc<T> changes the definition so that multiple variables can all hold a stake in the same heap-allocated list node. Instead of a Box<List>, the Cons variant stores an Rc<List>:

use std::rc::Rc;
enum List {
    Cons(i32, Rc<List>),
    Nil,
}
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));
}

This compiles without complaint. a is an Rc<List> that owns the list (5, (10, Nil)) on the heap. When b and c are created, they don’t try to take a. They call Rc::clone, which takes a shared reference to the Rc and increments its internal reference count. Both b and c now point to the same heap allocation that a points to. The list data lives on until all three — a, b, and c — have been dropped.

The call Rc::clone(&a) is the idiomatic way to increase the count. You could write a.clone(), and it would work identically, but the convention exists for a reason: Rc::clone visually calls out that this is a cheap, reference-counting clone, not a deep copy of the underlying data. When you see Rc::clone in code, you know the cost is negligible regardless of how large the inner value is.

Cloning Rc<T> Increases the Reference Count

Watching the reference count change makes the ownership picture concrete. The method Rc::strong_count returns the current number of active Rc handles that share the allocation. The following example creates an Rc<List>, tracks the count as new owners enter and leave scope, and prints the tally at each step:

use std::rc::Rc;
enum List {
    Cons(i32, Rc<List>),
    Nil,
}
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 prints:

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

Expected output:

If your output matches the counts above, shared ownership is working correctly. The tally goes up with each Rc::clone and down when a cloned Rc is dropped.

When a is created, it is the sole owner of the inner list — count is 1. b joins the picture and the count becomes 2. Inside the inner block, c bumps the count to 3. The moment that block ends, c goes out of scope, its Drop implementation runs, and the count decreases to 2 automatically. No manual decrement call exists; the Drop trait handles it silently. When b and a are dropped at the end of main, the count reaches zero and the entire list is deallocated.

The method is named strong_count because Rc also tracks a weak count for Weak<T> pointers, which don’t prevent the value from being dropped. Weak references are used to break reference cycles and will be introduced in a later section. For now, strong_count tells you how many owning handles currently keep the allocation alive.

Rc<T> gives you shared access to data, but that access is always immutable. You can call methods or read fields through Rc — it implements Deref so &Rc<T> behaves like &T — but you cannot obtain a &mut T from an Rc<T> alone. If Rust allowed multiple owners to all have mutable access, two pieces of code could simultaneously read and write the same memory, introducing data races. This is a direct consequence of the rule that you can have either one mutable reference or many immutable references, but not both. Rc provides the “many immutable references” side of that rule.

Rc gives immutable access only:

If you need to mutate data that is shared via Rc, you’ll need to combine it with a type that allows interior mutability, such as RefCell<T>. That pattern is covered in the next section and unlocks shared mutable state without violating Rust’s borrowing rules — by checking them at runtime instead of compile time.

A related trap is that Rc can inadvertently create reference cycles. If two Rc values point to each other, the strong count never reaches zero, and the memory is leaked — safely, but still leaked. This is one of the rare ways Rust allows a memory leak without unsafe. The standard library provides Weak<T> for this reason, and we’ll examine cycles and their prevention when we get to RefCell and Weak.

Reference cycles can leak memory:

Building a cycle where Rc handles point to each other will cause a memory leak because the reference count never drops to zero. Use Weak<T> to create non-owning references that break the cycle. The topic is explored in detail in the section on preventing cycles.

The real-world need for Rc comes up whenever you have shared, read-only data with a lifetime that can’t be determined by a single owner at compile time. GUI frameworks often use reference counting for shared state that multiple widgets observe. Graph and tree structures where nodes are reachable from multiple paths rely on it. Configuration objects that several components reference fall into the same category. In all these cases, Rc gives you the flexibility of shared ownership while keeping the guarantee that the data is freed exactly when the last user goes away — and only within a single thread.


Rc<T> solves a narrow but essential problem: sharing data when you cannot statically decide who should own it. It does so with zero runtime overhead beyond the counter increment and decrement — no garbage collector, no tracing, no pauses. The cost is that you must stay within the guardrails: single-threaded only, immutable access only, and careful avoidance of cycles.