Shared ownership without copying
You are building a UI framework. A Button needs to know the current font size. The Label next to it needs the same font size. The Window containing them also needs to read the font size to calculate layout. You have one FontSize value, but three widgets need to hold onto it.
Rust's ownership rules scream "Move!" The moment you move the font size to the button, the label can't see it. The compiler blocks you. You need a way to share the value without copying it, and without fighting the borrow checker.
Rc<T> solves this with shared ownership. The name stands for "Reference Counted." Think of it like a group of roommates sharing an apartment. The apartment is the data, sitting on the heap. Each roommate holds a key. That key is an Rc. When a new roommate moves in, you don't build a second apartment. You just cut a new key. The apartment stays occupied as long as at least one roommate has a key. When the last roommate moves out and returns their key, the lease ends and the apartment gets reclaimed.
Rc breaks the single-owner rule by trading compile-time guarantees for runtime counting. You get shared access, but you pay with heap allocation and counter overhead.
How Rc works
Rc::new(value) puts your value on the heap, alongside a counter that starts at one. When you call Rc::clone, you don't copy the value. You bump the counter and hand back another Rc pointing at the same heap allocation. Each Rc that goes out of scope decrements the counter. When the counter hits zero, the value gets dropped and the memory is freed.
use std::rc::Rc;
fn main() {
// Create the value on the heap. The Rc holds a pointer and a counter starting at 1.
let data = Rc::new(String::from("shared config"));
// Clone the Rc, not the String. This bumps the counter to 2.
// The String data is not copied. Only the pointer and counter update.
let clone = Rc::clone(&data);
// strong_count reads the counter. Both Rc instances point to the same allocation.
println!("Count: {}", Rc::strong_count(&data)); // Prints 2
}
You will see Rc::clone(&data) in the code above. You could write data.clone(), and it compiles. The community prefers the explicit Rc::clone form. Why? Because data.clone() looks like it might be doing a deep copy of the string. The explicit form signals to the reader: "I'm cloning the pointer, not the payload." It saves future-you from debugging a performance bug where you thought you were sharing data but accidentally duplicated a megabyte of text.
Under the hood, Rc<T> allocates a single heap block. This block contains the reference counter followed immediately by your value. The Rc struct itself is just a pointer to this block. This layout ensures cache locality. When you access the value, the counter is nearby. It also means cloning an Rc is just copying a pointer. There is no second allocation for the counter.
The single-block layout makes Rc efficient for cloning. You are copying a pointer, not data. The cost is the allocation, not the sharing.
Real-world usage
Graphs are the classic use case for Rc. In a graph, nodes can have multiple parents. A single node might be referenced by three different branches. If you store nodes by value, you have to clone the node data every time you add an edge. That breaks the single source of truth. If you update the node in one branch, the other branches still hold stale copies.
Rc lets you share the node. Every edge holds an Rc to the child node. The node lives in one place. Updates are visible to all parents.
use std::rc::Rc;
/// A node in a graph that can be referenced by multiple other nodes.
struct GraphNode {
label: String,
// Use Rc to allow multiple parents to point to the same child.
children: Vec<Rc<GraphNode>>,
}
fn main() {
// Create a shared node on the heap.
let shared_node = Rc::new(GraphNode {
label: String::from("Root"),
children: Vec::new(),
});
// Create two parents that both reference the shared node.
// Rc::clone bumps the counter for shared_node.
let parent_a = Rc::new(GraphNode {
label: String::from("Parent A"),
children: vec![Rc::clone(&shared_node)],
});
let parent_b = Rc::new(GraphNode {
label: String::from("Parent B"),
children: vec![Rc::clone(&shared_node)],
});
// The shared node has three owners: shared_node, parent_a, and parent_b.
println!("Reference count: {}", Rc::strong_count(&shared_node)); // Prints 3
}
This pattern appears in UI trees, parser caches, and dependency injection graphs. Any structure where nodes are shared and immutable fits Rc perfectly.
Pitfalls and compiler errors
Rc<T> has two major traps.
The first trap is cycles. If A holds an Rc<B> and B holds an Rc<A>, the counter never hits zero. You have a memory leak. Rust does not prevent this at compile time. The borrow checker cannot track reference counts across the heap. You have to use Weak references to break cycles. A Weak reference points to the same heap block but does not increment the strong count. It only increments a weak count. The value drops when the strong count hits zero, even if weak references exist. You must upgrade a Weak to an Option<Rc<T>> to use it. This returns None if the value has already been dropped.
The second trap is mutability. Rc<T> provides shared read access. You cannot mutate the data through an Rc. If you try to change a field, the compiler rejects you with E0596 (cannot borrow as mutable). The compiler enforces that shared references are immutable. This is a safety feature. If two owners could mutate the data, you would get data races or inconsistent state. To get mutability with shared ownership, you need interior mutability, usually Rc<RefCell<T>>. RefCell checks borrowing rules at runtime. It panics if you violate them.
Rc gives you shared ownership, not a free pass. Cycles leak memory. Mutation requires interior mutability. Send requires Arc. Respect the constraints, or the runtime will punish you.
Another error you might hit is E0277 (trait bound not satisfied) if you try to send an Rc across threads. Rc does not implement Send. The compiler will block you. This is good. It prevents data races. Rc uses non-atomic operations for the counter, which is faster but unsafe for concurrent access. If you need threads, the compiler error guides you to Arc.
Decision matrix
Use Rc<T> when you need multiple owners of immutable data within a single thread. Use Rc<T> for graph structures, UI trees, or parser caches where nodes are shared but never change.
Use Arc<T> when the data must cross thread boundaries. Arc stands for Atomic Reference Counted. It uses atomic operations for the counter, which is slower but safe for concurrent access. Rc is faster but panics if you try to send it across threads.
Use Rc<RefCell<T>> when you need shared ownership and interior mutability within a single thread. RefCell checks borrowing rules at runtime. This pattern is common for mutable UI state or game objects that multiple systems need to update.
Use Arc<Mutex<T>> when you need shared ownership and mutation across threads. Mutex provides exclusive access at runtime. This is the heavy-weight solution for concurrent mutable state.
Reach for plain references &T when lifetimes are simple and you don't need to store the reference in a struct. References are zero-cost and the borrow checker handles the safety. Rc adds heap allocation and counter overhead. Only pay that cost when you actually need shared ownership.
Pick the tool that matches your mutability and threading needs. Adding RefCell or Mutex changes the performance profile and error surface. Start with Rc<T> and add complexity only when the compiler forces you.