Telex logo Telex

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:

When not to use it:

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.