Rc<T> in Rust: vs Arc & Weak References Explained

Max WellsMax WellsFounder of Rustify

TL;DR: Rc<T> (Reference Counted) lets multiple parts of single-threaded code share ownership of a value. It tracks how many clones exist; when the count reaches zero the value is dropped. Unlike Arc<T>, Rc is not thread-safe, it has no atomic overhead. Use Rc for single-threaded shared ownership (tree nodes, graph edges), Arc when you need to share across threads.


What Is Rc<T> in Rust?

Rc<T> is a smart pointer that enables multiple-owner shared access to a heap-allocated value within a single thread, tracked via reference counting.

use std::rc::Rc;
 
fn main() {
    let a = Rc::new(String::from("shared value"));
 
    let b = Rc::clone(&a); // increments reference count; no data copy
    let c = Rc::clone(&a);
 
    println!("ref count: {}", Rc::strong_count(&a)); // 3
    println!("{a}, {b}, {c}");
 
    drop(c);
    println!("ref count after drop: {}", Rc::strong_count(&a)); // 2
} // a and b drop here; count reaches 0; value is freed

Rc::clone is cheap, it just increments a counter. The actual data is stored once on the heap.


How Is Rc Different From Arc?

Rc uses non-atomic reference counting (cheaper, single-thread only). Arc uses atomic operations (thread-safe but slightly more expensive). The API is identical.

Rc<T>Arc<T>
Thread-safe❌ (!Send, !Sync)
Reference countNon-atomic (cheap)Atomic (small overhead)
Use caseSingle-threaded sharingMulti-threaded sharing
Combined withRefCell<T>Mutex<T> or RwLock<T>
use std::rc::Rc;
use std::sync::Arc;
 
// Rc; fine in single-threaded code
let rc = Rc::new(42);
 
// Arc; required when moving across thread boundaries
let arc = Arc::new(42);
std::thread::spawn(move || println!("{arc}")); // Arc is Send
// std::thread::spawn(move || println!("{rc}")); // Error: Rc is !Send

How Do You Get Mutability With Rc?

Rc<T> gives shared (immutable) access. For mutation, combine it with RefCell<T>, this is the Rc<RefCell<T>> pattern for interior mutability in single-threaded code.

use std::rc::Rc;
use std::cell::RefCell;
 
fn main() {
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
 
    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);
 
    // Multiple owners, all can mutate
    a.borrow_mut().push(4);
    b.borrow_mut().push(5);
 
    println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
}

Borrow rules are enforced at runtime (panics if you have two mutable borrows simultaneously), not compile time.


When Should You Use Rc?

Use Rc when you have a data structure where ownership isn't clear, multiple parts need equal ownership of the same data, all within one thread.

Common use cases:

  • Tree/graph nodes that may have multiple parents
  • Shared config or read-only data within one thread
  • Implementing a simple arena or reference graph
  • GUI frameworks where widgets may reference shared state
use std::rc::Rc;
 
// Linked list node that may be referenced from multiple places
struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}
 
fn main() {
    let leaf = Rc::new(Node { value: 1, children: vec![] });
 
    // Two parent nodes share the same leaf
    let parent_a = Node { value: 2, children: vec![Rc::clone(&leaf)] };
    let parent_b = Node { value: 3, children: vec![Rc::clone(&leaf)] };
}

What Are Weak References?

Rc::downgrade creates a Weak<T>, a non-owning reference that doesn't prevent the value from being dropped. Use Weak to break reference cycles.

use std::rc::{Rc, Weak};
use std::cell::RefCell;
 
struct Node {
    value: i32,
    parent: Option<Weak<RefCell<Node>>>, // weak; doesn't keep parent alive
    children: Vec<Rc<RefCell<Node>>>,
}

If you have Rc<A> pointing to B and Rc<B> pointing back to A, neither will ever be dropped, a cycle. Make one direction Weak to break the cycle.


Frequently Asked Questions

Small overhead: each clone/drop increments/decrements a counter (non-atomic, very fast). Dereferencing Rc<T> itself has no overhead, it's just a pointer dereference. The main cost is the extra heap allocation for the reference count alongside the data.

No, Rc<T> is !Send and !Sync. The compiler will reject any attempt to send it to another thread. Use Arc<T> for thread-safe shared ownership.

Circular Rc references create memory leaks, the count never reaches zero, so the values are never dropped. Use Weak<T> for back-references in data structures to break cycles.

Yes, Rc is 100% safe Rust. Reference cycles cause memory leaks (not undefined behavior), and RefCell panics (not undefined behavior) if borrow rules are violated at runtime.


Sources


  • Arc: The thread-safe version of Rc
  • RefCell: Combined with Rc for interior mutability
  • Box: Single-ownership heap allocation (simpler than Rc)
  • Ownership: Rc exists to work around single-ownership restrictions

Keep Reading

Ready to Land a $80-120k Rust Job?