Rust Patterns That Matter #5: Shared Ownership
Post 5 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #4: Interior Mutability | Next: #6: Rc<RefCell<T>>
Rust enforces single ownership: one variable owns each value, and when that variable goes out of scope, the value is dropped. This is the foundation of Rust's memory safety without garbage collection. But trees, graphs, and callback registries all need multiple parts of the program to hold onto the same data. Single ownership doesn't cover this.
The motivation
You're building a tree structure. Each node has children, and you also want to keep a flat list of all nodes for fast iteration. The children are owned by their parents. The flat list also needs access to the same nodes.
struct Node {
value: String,
children: Vec<Box<Node>>,
}
fn main() {
let child = Box::new(Node {
value: "child".to_string(),
children: vec![],
});
let mut all_nodes = vec![child]; // child moved here
let parent = Node {
value: "parent".to_string(),
children: vec![child], // ERROR: use of moved value
};
}
The compiler:
error[E0382]: use of moved value: `child`
child moved into all_nodes. It's gone. You can't also
put it in the parent's children. Box enforces single ownership -
when you move a Box, the original variable is invalidated.
Why Rust does this
Single ownership answers a fundamental question: who frees this memory? With one
owner, the answer is always clear - the owner does, when it goes out of scope.
No double frees, no use-after-free, no garbage collector needed. Box<T>
is the direct expression of this: one pointer, one owner, one deallocation.
But when data is genuinely shared - a node referenced from both a tree and a flat list, a configuration struct used by multiple subsystems, a callback registered with several event sources - single ownership doesn't model reality.
The pattern: Rc<T>
Rc<T> is a reference-counted pointer. Multiple Rcs
can point to the same heap-allocated value. An internal counter tracks how many
Rcs exist. When the last one is dropped, the value is freed.
use std::rc::Rc;
struct Node {
value: String,
children: Vec<Rc<Node>>,
}
fn main() {
let child = Rc::new(Node {
value: "child".to_string(),
children: vec![],
});
let mut all_nodes = vec![Rc::clone(&child)]; // count: 2
let parent = Node {
value: "parent".to_string(),
children: vec![Rc::clone(&child)], // count: 3
};
// child, all_nodes[0], and parent.children[0]
// all point to the same Node
}
Rc::clone(&child) doesn't copy the Node. It increments
the reference count and returns a new Rc pointing to the same allocation.
This is cheap - a single atomic increment and a pointer copy.
The Rc::clone() convention
You'll see experienced Rust developers write Rc::clone(&x) instead of
x.clone(). Both do the same thing, but Rc::clone makes it
visually obvious that you're bumping a reference count, not deep-copying data.
When scanning code, x.clone() could be expensive (cloning a large struct);
Rc::clone(&x) is always cheap.
What you can't do
Rc gives you &T - shared, immutable access.
You can read through an Rc, but you can never get &mut T
from it. This is fundamental: multiple pointers to the same data plus mutation
equals data races and aliasing bugs. Rc prevents this by making the data
read-only.
let data = Rc::new("hello".to_string());
let alias = Rc::clone(&data);
// Reading: fine
println!("{}", *data);
// Mutating: no
// There's no way to get &mut String from Rc<String>
If you need shared ownership and mutation, you need to combine
Rc with RefCell -
the next post.
The tradeoffs
Rc has a small runtime cost: reference counting on clone and drop.
In practice this is negligible unless you're cloning in a tight inner loop.
The bigger constraint is that Rc is not thread-safe.
Its reference count is a plain integer, not an atomic. Sending an Rc
to another thread is a compile error - Rc is !Send.
If you need shared ownership across threads, use Arc
(#19), which uses atomic operations
for the counter.
Rc also can't detect reference cycles. If A holds an Rc
to B and B holds an Rc to A, neither will ever be freed. For
cycles, use Weak (a non-owning reference that doesn't prevent
deallocation) to break the cycle.
When to use it
Good uses:
- Tree structures where nodes are referenced from multiple places
- Shared configuration passed to multiple subsystems
- Callback registries where the same handler is registered in several places
- Any situation where you need multiple owners and single ownership doesn't model reality
When not to use it:
- If only one thing owns the data, use
Boxor direct ownership - it's simpler and has no reference-counting overhead - If you need thread safety, use
Arcinstead - If you need mutation,
Rcalone isn't enough - combine it withRefCell(#6)
What comes next
Rc gives you shared ownership but no mutation. RefCell
(#4) gives you mutation but
single ownership. The next post puts them together: Rc<RefCell<T>>
- shared ownership with interior mutability. This is the workhorse pattern for
single-threaded shared mutable state, and it's exactly what Telex uses for component
state.
See it in practice: Building a Chat Server #2: Rooms and Users uses this pattern for shared ownership of room data.
Telex