When one owner isn't enough
You are building a parser for a custom configuration format. The parser constructs a syntax tree where multiple nodes reference the same source string. You try to pass the string to the first node, and the compiler rejects you with E0382 (use of moved value). You move the string to the second node, and the first node can no longer access it. You feel stuck. The data exists, but Rust insists only one thing can own it. You want to share the data without copying it, because the string is large and copying hurts performance.
Rust's default rule is strict: every value has exactly one owner. The owner is responsible for cleaning the value up when its scope ends. This rule prevents double-free bugs and use-after-free errors. It also makes memory management deterministic. You can share ownership, but you have to opt in explicitly. The language provides smart pointers for this purpose. Rc<T> handles shared ownership in a single thread. Arc<T> handles shared ownership across threads.
The single-owner rule and why it exists
Rust enforces single ownership to keep the compiler's job simple and the runtime fast. If a value has one owner, the compiler knows exactly when to drop the value. It tracks the owner's scope. When the scope ends, the value is destroyed. No garbage collector is needed. No reference counting overhead is needed.
This model breaks down when you have a graph structure, a UI tree, or a shared cache. In these cases, no single node is the "main" owner. Multiple parts of the program need to access the same data, and the data must stay alive as long as anyone is using it. Rust solves this with reference counting. You wrap the value in a smart pointer that tracks how many owners exist. The value is dropped only when the count reaches zero.
Think of a house deed. Normally, one name is on the deed. If that person moves, the house goes with them. Multiple ownership is like a shared custody agreement. Everyone has a key. The house stays occupied as long as at least one person has a key. When the last person gives up their key, the house is cleared.
Opting in to shared ownership with Rc
Rc<T> stands for "reference counted." It wraps a value and maintains a counter of how many Rc instances point to that value. When you create an Rc, the counter starts at one. When you clone an Rc, the counter increments. When an Rc goes out of scope, the counter decrements. When the counter hits zero, the value is dropped.
Rc is designed for single-threaded use. It does not use atomic operations, so it is faster than Arc<T>. If you need to share data across threads, use Arc<T> instead. Arc uses atomic operations to update the counter safely in a concurrent environment. Atomic operations are more expensive, so stick to Rc when you are in a single thread.
Minimal example
use std::rc::Rc;
/// A shared configuration object.
struct Config {
/// Flag to enable verbose logging.
verbose: bool,
/// Connection string for the database.
db_url: String,
}
fn main() {
// Create the config on the heap with an Rc wrapper.
// The reference count starts at 1.
let config = Rc::new(Config {
verbose: true,
db_url: "postgres://localhost/app".to_string(),
});
// Clone the Rc to share ownership with a subsystem.
// This increments the count to 2 without copying the data.
let subsystem_config = Rc::clone(&config);
// Both variables point to the same Config instance.
// The data stays alive until both go out of scope.
println!("Main verbose: {}", config.verbose);
println!("Subsystem URL: {}", subsystem_config.db_url);
}
Convention aside: The community prefers Rc::clone(&config) over config.clone(). Both compile and both work. The explicit form signals to readers that you are cloning the reference count, not the underlying data. config.clone() looks like a deep clone but isn't. Using Rc::clone prevents confusion.
What happens under the hood
When you call Rc::new(value), Rust allocates memory on the heap. The allocation holds the value and a counter. The Rc itself is small. It contains a pointer to the heap allocation and, in debug mode, a copy of the counter. In release mode, the counter lives only on the heap.
When you call Rc::clone, Rust does not copy the value. It increments the counter on the heap and returns a new Rc pointing to the same allocation. This is a shallow clone. The cost is a single integer increment.
When an Rc goes out of scope, the Drop implementation decrements the counter. If the counter is not zero, the Rc is discarded and the heap allocation stays alive. If the counter hits zero, the heap allocation is freed and the value is dropped.
This mechanism ensures the value lives exactly as long as needed. It also introduces overhead. Every clone and drop touches the heap counter. If you are cloning millions of times in a tight loop, profile the cost. Reference counting is cheap, but not free.
Realistic example: Shared event handlers
In a UI framework, multiple widgets might share the same event handler. You want to avoid copying the handler for every widget. You use Rc to share the handler.
use std::rc::Rc;
/// A handler that processes click events.
struct ClickHandler {
/// Number of times the handler has been invoked.
click_count: u32,
}
impl ClickHandler {
/// Creates a new handler.
fn new() -> Self {
ClickHandler { click_count: 0 }
}
/// Simulates handling a click.
fn handle(&self) {
println!("Click handled. Count: {}", self.click_count);
}
}
/// A widget that holds a reference to a shared handler.
struct Button {
/// The handler shared with other widgets.
handler: Rc<ClickHandler>,
}
fn main() {
// Create a shared handler.
let handler = Rc::new(ClickHandler::new());
// Create buttons that share the handler.
let button1 = Button {
handler: Rc::clone(&handler),
};
let button2 = Button {
handler: Rc::clone(&handler),
};
// Both buttons use the same handler instance.
button1.handler.handle();
button2.handler.handle();
// The handler stays alive because both buttons hold an Rc.
// It is dropped only when both buttons go out of scope.
}
This pattern scales to complex graphs. Nodes can share edges. Components can share resources. The key is that the data is read-only. Rc<T> provides immutable access. If you need to mutate the data, you need a different tool.
Pitfalls and compiler errors
Shared ownership introduces specific failure modes. The compiler catches some at compile time. Others require careful design.
Mutability requires interior mutability
Rc<T> guarantees immutable access. If you try to mutate the data through an Rc, the compiler rejects you with E0596 (cannot borrow as mutable). This rule prevents data races. If two Rc instances could mutate the same data, you could have aliasing issues.
To mutate shared data, you need interior mutability. Wrap the value in RefCell<T>. Rc<RefCell<T>> allows shared ownership with mutable access. The borrow checker moves from compile time to runtime. You pay a small runtime cost for the flexibility.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Rc<RefCell<T>> allows mutation through shared ownership.
let data = Rc::new(RefCell::new(vec![1, 2, 3]));
// Borrow mutably to push a value.
// This panics at runtime if the value is already borrowed.
data.borrow_mut().push(4);
println!("{:?}", data.borrow());
}
Mutability through Rc requires RefCell. The borrow checker moves to runtime, not away.
Cycles cause memory leaks
Reference counting cannot handle cycles. If A holds Rc<B> and B holds Rc<A>, the counter never drops to zero. The memory leaks. This is common in graph structures where nodes reference each other.
To break cycles, use Weak<T>. Weak points to the same allocation but does not increment the counter. You can upgrade a Weak to an Rc if the value is still alive. Use Weak for back-references in trees or graphs.
Cycles kill Rc. Break them with Weak or redesign the graph.
Performance overhead
Every Rc::clone and Rc drop touches the heap counter. If you are cloning in a tight loop, the overhead adds up. Profile before you wrap everything in Rc. Sometimes a deep clone is faster than reference counting if the value is small and the clone count is high.
Reference counting is cheap, but not free. Measure before you wrap.
Decision: Choosing the right sharing strategy
Rust offers several ways to share data. Pick the tool that matches your constraints.
Use &T when the borrower lives shorter than the owner. Borrowing is free and safe. It does not extend the lifetime of the data.
Use Rc<T> when multiple owners exist in a single thread and the data is read-only. Rc is faster than Arc because it avoids atomic operations.
Use Arc<T> when multiple owners exist across threads. Arc uses atomic operations to update the counter safely in a concurrent environment.
Use Rc<RefCell<T>> when you need shared mutable state in a single thread. The borrow checker moves to runtime. You pay a small cost for flexibility.
Use Arc<Mutex<T>> when you need shared mutable state across threads. The mutex serializes access. You pay the cost of locking.
Use Clone when you need independent copies that can diverge. Cloning copies the data. Changes to one copy do not affect the other.
Use Weak<T> when you need a back-reference that does not keep the data alive. Weak breaks cycles in graphs and trees.
Trust the borrow checker. It usually has a point. If you are fighting it, you might be using the wrong tool.