RustRc<T> — Reference Counting

Rc<T> — Reference Counting

Rust's ownership model normally allows exactly one owner per value. That is usually the right default, but sometimes multiple parts of your program need to read the same data, and none of them is a clear "primary" owner.

Rc<T> (Reference Counted) solves this problem. It keeps a count of how many owners exist and only frees the underlying data when that count reaches zero.

The Problem Rc Solves

Imagine a graph where several nodes all reference the same shared configuration. With plain ownership you would have to pick one owner, which forces every other node to borrow — and borrow lifetimes must be tracked at compile time. When the relationship is dynamic (built at runtime), that becomes unwieldy.

Rc<T> lets multiple owners exist simultaneously without complex lifetime annotations. The trade-off is that it only works on a single thread.

Creating and Cloning an Rc

Create an Rc<T> with Rc::new(). To create an additional owner, call Rc::clone(&rc). This does not clone the underlying data — it only increments the reference count, which is a very cheap operation.

RUST
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("hello"));
    println!("count after creating a: {}", Rc::strong_count(&a)); // 1

    let b = Rc::clone(&a);
    println!("count after cloning to b: {}", Rc::strong_count(&a)); // 2

    {
        let c = Rc::clone(&a);
        println!("count after cloning to c: {}", Rc::strong_count(&a)); // 3
    } // c is dropped here

    println!("count after c is dropped: {}", Rc::strong_count(&a)); // 2

    // Both a and b point to the same String
    println!("a = {}", a);
    println!("b = {}", b);
} // b dropped -> count 1, then a dropped -> count 0 -> String is freed
count after creating a: 1
count after cloning to b: 2
count after cloning to c: 3
count after c is dropped: 2
a = hello
b = hello
Note
Always use Rc::clone(&rc) rather than rc.clone(). Both work, but the explicit form makes it clear to readers that you are incrementing a reference count, not performing an expensive deep clone of the underlying data.
Rc Gives Only Immutable Access

Rc<T> does not allow mutation through the shared reference. If multiple owners could mutate the same data simultaneously, you would have data races even on a single thread, since mutation could invalidate other owners' views.

RUST
use std::rc::Rc;

fn main() {
    let a = Rc::new(vec![1, 2, 3]);
    let b = Rc::clone(&a);

    // Reading through either handle is fine
    println!("a: {:?}", a);
    println!("b: {:?}", b);

    // Mutation is not allowed directly through Rc:
    // a.push(4); // ERROR: cannot borrow data in an Rc as mutable
}
Tip
To get both shared ownership and mutation, combine Rc with RefCell: use Rc<RefCell<T>>. This pattern is covered on the RefCell page.
Rc is NOT Thread-Safe

Rc<T> uses a plain (non-atomic) integer for its reference count. Incrementing and decrementing that integer from multiple threads simultaneously is a data race. The compiler prevents you from sending an Rc to another thread.

For multi-threaded shared ownership, use Arc<T> (Atomically Reference Counted) instead — it has the same API but uses atomic operations for the count.

Type

Thread-safe

Count update

Use when

Rc<T>

No

Plain integer (fast)

Single-threaded shared ownership

Arc<T>

Yes

Atomic (slightly slower)

Multi-threaded shared ownership

Reference Cycles and Memory Leaks

Rc<T> frees data when the reference count reaches zero. But if two values hold Rc pointers to each other, their counts never reach zero — even after all external references are gone. This is a reference cycle and it leaks memory.

RUST
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

fn main() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: None }));

    // a points to b
    a.borrow_mut().next = Some(Rc::clone(&b));
    // b points back to a — creating a cycle
    b.borrow_mut().next = Some(Rc::clone(&a));

    println!("a strong count: {}", Rc::strong_count(&a)); // 2
    println!("b strong count: {}", Rc::strong_count(&b)); // 2

    // When a and b go out of scope, counts drop to 1 (not 0) — memory leak!
}
Warning
Reference cycles with Rc cause memory leaks that the Rust compiler cannot detect. Always review code where Rc values can point back to each other. The standard fix is to break the cycle with Weak<T>.
Weak<T> — Breaking Reference Cycles

Weak<T> is a non-owning reference. It does not increment the strong count, so it does not prevent the value from being dropped. To access the value through a Weak<T> you call .upgrade(), which returns Option<Rc<T>>None if the value has already been freed.

Use Rc::downgrade(&rc) to create a Weak<T> from an Rc<T>.

RUST
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    // Weak reference — does not keep 'parent' alive
    parent: Option<Weak<RefCell<Node>>>,
    children: Vec<Rc<RefCell<Node>>>,
}

fn main() {
    let parent = Rc::new(RefCell::new(Node {
        value: 1,
        parent: None,
        children: vec![],
    }));

    let child = Rc::new(RefCell::new(Node {
        value: 2,
        parent: Some(Rc::downgrade(&parent)), // weak — no cycle
        children: vec![],
    }));

    parent.borrow_mut().children.push(Rc::clone(&child));

    println!("parent strong count: {}", Rc::strong_count(&parent)); // 1
    println!("child strong count:  {}", Rc::strong_count(&child));  // 2

    // Access parent through weak reference
    if let Some(p) = child.borrow().parent.as_ref().and_then(|w| w.upgrade()) {
        println!("child's parent value: {}", p.borrow().value);
    }
} // Both nodes are freed correctly — no cycle
parent strong count: 1
child strong count:  2
child's parent value: 1
Rc::strong_count and Rc::weak_count

RUST
use std::rc::Rc;

fn main() {
    let value = Rc::new(42);
    let strong1 = Rc::clone(&value);
    let weak1 = Rc::downgrade(&value);
    let weak2 = Rc::downgrade(&value);

    println!("strong: {}", Rc::strong_count(&value)); // 2
    println!("weak:   {}", Rc::weak_count(&value));   // 2

    drop(strong1);
    println!("strong after drop: {}", Rc::strong_count(&value)); // 1

    // Weak references can still try to upgrade while strong count > 0
    match weak1.upgrade() {
        Some(v) => println!("upgraded: {}", v),
        None    => println!("value was dropped"),
    }

    drop(value); // last strong owner dropped

    // Now upgrade returns None
    match weak2.upgrade() {
        Some(v) => println!("upgraded: {}", v),
        None    => println!("value was dropped"),
    }
}
strong: 2
weak:   2
strong after drop: 1
upgraded: 42
value was dropped
Real-World Use Cases for Rc
  • Shared configuration — multiple subsystems read the same parsed config without copying it.

  • Abstract Syntax Tree nodes — parent and sibling nodes share references during tree traversal.

  • Graph data structures — vertices hold shared references to edges and neighbouring vertices.

  • Observer pattern — a subject holds weak references to observers; observers hold strong Rc to shared state.

Summary: Rc<T> at a Glance

RUST
use std::rc::{Rc, Weak};

fn main() {
    // Create
    let owner = Rc::new(String::from("shared data"));

    // Clone — cheap, increments count
    let co_owner = Rc::clone(&owner);

    println!("strong: {}", Rc::strong_count(&owner)); // 2
    println!("weak:   {}", Rc::weak_count(&owner));   // 0

    // Downgrade to weak
    let weak: Weak<String> = Rc::downgrade(&owner);
    println!("weak:   {}", Rc::weak_count(&owner));   // 1

    // Drop one strong owner
    drop(co_owner);
    println!("strong: {}", Rc::strong_count(&owner)); // 1

    // Upgrade weak -> Some because one strong owner remains
    println!("upgrade: {:?}", weak.upgrade());

    // Drop the last strong owner
    drop(owner);

    // Upgrade weak -> None because the value is gone
    println!("upgrade after drop: {:?}", weak.upgrade());
}
strong: 2
weak:   0
weak:   1
strong: 1
upgrade: Some("shared data")
upgrade after drop: None
Success
Use Rc<T> whenever multiple parts of a single-threaded program need to share ownership of the same data. Pair it with RefCell<T> for shared mutation, and use Weak<T> to break cycles. For multi-threaded code, reach for Arc<T> instead.