Shared Ownership and Interior Mutability
Chapter 2 claimed that the long-lived data in most programs forms a tree, and that a design wanting a graph is usually a design avoiding a decision. That is true often enough to be worth saying first, and it is not true always. Some structures are genuinely graphs. A scene graph where nodes know their parents. A cache several subsystems read and one updates. An interpreter’s environment chain. A GUI where a button needs to reach the model it edits.
For those, Rust provides shared ownership and interior mutability. This chapter is about what they are, what they cost, and the discipline that keeps them from becoming the way you write everything.
The discipline matters because these tools are the exit from the model the rest of the book describes. Reach for them when the design needs them and they are sound engineering. Reach for them the first time the borrow checker objects and you have reinvented the language you came from, with worse syntax and a runtime panic where the compile error used to be.
Rc: many owners, counted
Rc<T> is a reference-counted pointer. Cloning it produces another handle to the
same value and increments a count; dropping a handle decrements it; the value is
dropped when the count reaches zero.
use std::rc::Rc;
struct Config {
endpoint: String,
}
struct Client {
name: &'static str,
config: Rc<Config>,
}
fn main() {
let config = Rc::new(Config { endpoint: String::from("https://api.example.com") });
println!("count after creation: {}", Rc::strong_count(&config));
let clients = vec![
Client { name: "reader", config: Rc::clone(&config) },
Client { name: "writer", config: Rc::clone(&config) },
];
println!("count with two clients: {}", Rc::strong_count(&config));
for client in &clients {
println!("{} -> {}", client.name, client.config.endpoint);
}
drop(clients);
println!("count after dropping clients: {}", Rc::strong_count(&config));
}
Ownership is no longer a single place, so the destruction schedule is no longer readable off the source. That is the trade: you gave up knowing statically when the value dies in exchange for not having to decide who owns it.
Two conventions are worth adopting. Write Rc::clone(&x) rather than x.clone()
— they do the same thing, and the explicit form makes it visible in review that
this is a cheap refcount bump rather than a deep copy. And note that Rc is
single-threaded: it uses non-atomic increments, which is why it is fast and why
the compiler will not let you send one to another thread. The threaded version is
Arc, and chapter 14 covers it.
The restriction that matters for design is that Rc<T> gives you shared
access, which means read-only access. There is no Rc::get_mut unless you are
the only owner. This follows directly from the rule in chapter 3: many readers or
one writer, and Rc is how you get many readers.
Interior mutability moves the check to runtime
When you need many owners and mutation, you need a type that can hand out an
exclusive reference from behind a shared one. That inversion is called interior
mutability, and RefCell<T> is the general-purpose version:
use std::cell::RefCell;
fn main() {
let log = RefCell::new(Vec::new());
log.borrow_mut().push("started");
log.borrow_mut().push("working");
println!("{} entries: {:?}", log.borrow().len(), log.borrow());
}
borrow() returns a Ref<T>, which acts like &T. borrow_mut() returns a
RefMut<T>, which acts like &mut T. RefCell keeps a counter and enforces the
same rule the compiler would have: many shared borrows or one exclusive borrow.
The difference is when it is enforced. Break the rule and the program panics:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let reader = data.borrow();
let mut writer = data.borrow_mut();
writer.push(4);
println!("{reader:?}");
}
That panics with already borrowed: BorrowMutError. The bug is the same one
chapter 3 described, and it is now a runtime failure in production rather than a
build failure on your machine. That is the price of interior mutability, and it
is why RefCell is a tool for cases where the compiler genuinely cannot verify a
pattern that is nevertheless correct, rather than a way to stop it asking.
The guards are ordinary values with Drop implementations, so a borrow lasts
until its guard goes out of scope. Most RefCell panics come from holding one
longer than intended:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let total: i32 = data.borrow().iter().sum();
data.borrow_mut().push(total);
println!("{:?}", data.borrow());
}
Reading the sum into a local ends the shared borrow before the mutable one
begins. Writing data.borrow_mut().push(data.borrow().iter().sum()) compiles and
panics, because the argument’s borrow is still alive when borrow_mut runs. The
discipline is to keep borrows short and never to hold one across a call that
might borrow again.
Note
For a
Copytype that only needs get and set,Cell<T>is simpler and cannot panic: it moves values in and out rather than handing out references, so there is nothing to track. A counter, a flag, a cached length —Cell<u32>andCell<bool>do the job with no runtime bookkeeping at all.
The combination, and its reputation
Putting the two together gives Rc<RefCell<T>>: shared ownership of something
mutable. It is the shape newcomers converge on, because it makes Rust behave like
a language with references and mutable objects.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Account {
balance: i64,
}
fn main() {
let account = Rc::new(RefCell::new(Account { balance: 0 }));
let teller = Rc::clone(&account);
let auditor = Rc::clone(&account);
teller.borrow_mut().balance += 100;
teller.borrow_mut().balance -= 30;
println!("auditor sees {}", auditor.borrow().balance);
}
This works and is sometimes right. What makes it an anti-pattern when it is the default is that it gives up both guarantees at once: no static knowledge of who owns the value, and no static knowledge of who is writing to it. Everything chapters 2 and 3 bought is now checked at runtime, in exchange for a design you did not have to think about.
Before writing it, walk down the ladder. Can one place own the value, with
everyone else borrowing? Can the parts that need it take it in turn? Can the
thing be identified rather than pointed at, so that one owner holds the storage
and others hold identifiers? Can the mutation be gathered into one place — a
single update function that everyone calls — rather than distributed among
holders? Each of those is a real design, and each keeps the checks at compile
time. Rc<RefCell<T>> is what you use when all of them have been considered and
the structure really is a shared mutable graph.
Cycles leak
Reference counting has a known failure, and Rust does not fix it: two values that
hold Rc handles to each other keep each other’s count above zero, so neither is
ever dropped.
This is not undefined behavior and not a safety violation — Rust’s safety guarantee is about memory corruption, not about leaks — but it is still a bug, and the classic case is a tree whose children point back at their parents.
The tool is Weak<T>: a handle that does not contribute to the count and does
not keep the value alive. Getting at the value requires upgrade(), which
returns an Option<Rc<T>> and tells you whether the value is still there.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: String,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(name: &str) -> Rc<Node> {
Rc::new(Node {
name: String::from(name),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<Node>, child: Rc<Node>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(child);
}
fn path(&self) -> String {
match self.parent.borrow().upgrade() {
Some(parent) => format!("{}/{}", parent.path(), self.name),
None => self.name.clone(),
}
}
}
fn main() {
let root = Node::new("root");
let docs = Node::new("docs");
let chapter = Node::new("ch01.md");
Node::add_child(&root, Rc::clone(&docs));
Node::add_child(&docs, Rc::clone(&chapter));
println!("{}", chapter.path());
println!("root has {} child", root.children.borrow().len());
}
The direction of ownership is the design decision, and it is the same decision
you would make in any language that had to answer it: the parent owns its
children, the child refers to its parent. Delete the root and the subtree
goes with it; delete a child and the parent survives. Writing it with Rc in both
directions compiles, runs, and leaks everything.
The other answer: identifiers and an arena
The Rc-and-Weak tree above is the direct translation of a pointer-based
design. There is a second approach that is often better in Rust and is
underappreciated by people arriving from languages where it is unusual: store the
nodes in one collection and refer to them by index.
struct Tree {
nodes: Vec<Node>,
}
struct Node {
name: String,
parent: Option<usize>,
children: Vec<usize>,
}
impl Tree {
fn new(root: &str) -> Tree {
Tree {
nodes: vec![Node { name: String::from(root), parent: None, children: Vec::new() }],
}
}
fn add(&mut self, parent: usize, name: &str) -> usize {
let id = self.nodes.len();
self.nodes.push(Node {
name: String::from(name),
parent: Some(parent),
children: Vec::new(),
});
self.nodes[parent].children.push(id);
id
}
fn path(&self, id: usize) -> String {
match self.nodes[id].parent {
Some(parent) => format!("{}/{}", self.path(parent), self.nodes[id].name),
None => self.nodes[id].name.clone(),
}
}
}
fn main() {
let mut tree = Tree::new("root");
let docs = tree.add(0, "docs");
let chapter = tree.add(docs, "ch01.md");
println!("{}", tree.path(chapter));
println!("{} nodes", tree.nodes.len());
}
Everything is owned by the Tree, so there is no reference counting, no interior
mutability, no cycle problem, and no runtime borrow tracking. Nodes sit
contiguously in memory, which makes traversal faster than chasing pointers.
Serializing the structure is trivial, because indices survive a round trip
through a file and pointers do not. The structure can be cloned, sent
between threads, or snapshotted by copying one Vec.
The costs are real and worth stating: an index is not checked for validity the
way a pointer is, so a stale index after a removal reads the wrong node rather
than failing. Production crates such as slotmap and generational-arena solve
that with generation counters, and are worth reaching for when removal is part of
the design.
For graphs and trees that live in one place and are traversed often — parsers,
ECS systems, dependency graphs, document models — this pattern is usually the
better Rust design. Try it before Rc<RefCell<T>>.
Conclusion
Shared ownership and interior mutability are the tools for structures that are
genuinely not trees. Rc gives many owners and read-only access. RefCell moves
the borrow rule from compile time to runtime, with a panic where there used to be
an error. Together they reproduce the reference-and-mutate model of other
languages, and reproduce its debugging experience as well.
The discipline is to earn them. Single ownership with borrowing, values taken in
turn, identifiers instead of pointers, and mutation gathered into one place all
keep the checks static, and they cover most of what looks at first like a case
for sharing. When the structure really is a shared mutable graph, use the tools
deliberately, decide which direction owns and which refers, and use Weak for
the back edges.
The index-and-arena design deserves a first look rather than a last one. It is faster, it serializes, it has no cycles to leak, and it keeps a graph-shaped structure inside a single owner.
The next chapter goes back to code that most Rust programs are made of, and to the abstraction that makes it fast and readable at once.
Questions to think through
Find a place in a system you know where two objects hold references to each other. Which one conceptually owns the other, and what would happen today if the owner were destroyed first?
RefCell converts a compile error into a runtime panic. Under what circumstances
is that a good trade, and what does it require of your test coverage?
The arena version of the tree uses usize indices, which are not validated. What
would go wrong if a node were removed, and what does a generation counter add
that a plain index does not?