Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Copy type 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> and Cell<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?