Smart pointers and interior mutability

Box, Rc, RefCell, Arc and Mutex, Cow, reference cycles and Weak, and how to choose the ownership pattern for a shared data model.

Box, Rc and RefCell

use std::cell::RefCell;
use std::rc::Rc;

// Box: one owner, heap allocation, fixed size for recursive types
enum Tree {
    Leaf(i32),
    Node(Box<Tree>, Box<Tree>),
}

// Rc: several owners, single-threaded reference counting
#[derive(Debug)]
struct Node {
    label: String,
    children: RefCell<Vec<Rc<Node>>>,     // interior mutability
}

fn main() {
    let leaf = Rc::new(Node { label: "leaf".into(), children: RefCell::new(vec![]) });
    let root = Rc::new(Node { label: "root".into(), children: RefCell::new(vec![]) });

    root.children.borrow_mut().push(Rc::clone(&leaf));   // mutate through &root
    println!("{:?} strong={}", root, Rc::strong_count(&root));

    // borrow() panics if a mutable borrow is already live
    {
        let _guard = leaf.children.borrow();
        // leaf.children.borrow_mut();   // would panic at run time
    }

    let boxed = Box::new(42);
    println!("{} {}", boxed, (*boxed) + 1);
    let t = Tree::Node(Box::new(Tree::Leaf(1)), Box::new(Tree::Leaf(2)));
    match t {
        Tree::Node(a, _b) => println!("{:?}", matches!(*a, Tree::Leaf(_))),
        Tree::Leaf(_) => {}
    }
}
  • Box<T> is the cheapest smart pointer: one indirection, one owner, no counters. Use it for recursive types, for large values you want to move cheaply, and for trait objects.
  • Rc<T> shares ownership by counting references on the heap and dropping the value at zero. It is not thread-safe, so it cannot cross a thread boundary — the compiler enforces that.
  • RefCell<T> moves the borrow rules from compile time to run time. You get flexibility, and you get a panic instead of a compile error when you break the rule.
  • borrow() and borrow_mut() return guards. Holding two mutable guards at once panics, so keep them small and never hold one across a call that might borrow again.
  • The usual combination is Rc<RefCell<T>> for a mutable shared structure in one thread, and Arc<Mutex<T>> for the same across threads.

Arc, Mutex and Cow

use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

    for _ in 0..4 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // lock returns a guard; the guard releases on drop
                let mut n = c.lock().unwrap();
                *n += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("{}", counter.lock().unwrap());

    // Cow: borrowed until it must be modified
    fn normalise(input: &str) -> Cow<'_, str> {
        if input.contains(' ') {
            Cow::Owned(input.replace(' ', "_"))
        } else {
            Cow::Borrowed(input)      // no allocation on the common path
        }
    }
    println!("{} {}", normalise("a b"), normalise("ab"));
}
TypeThread-safeUse it for
Box<T>YesRecursive types, cheap moves, Box<dyn Trait>
Rc<T>NoShared ownership inside one thread
Arc<T>YesShared ownership across threads; the atomic counter is the cost
RefCell<T>NoMutation through a shared reference, checked at run time
Mutex<T>YesMutation across threads; the lock is the data, not separate from it
Cow<'a, T>Depends on TA value that is usually borrowed and occasionally modified
  • RwLock beats Mutex when reads vastly outnumber writes; otherwise the extra bookkeeping makes it slower.
  • Mutex poisoning: if a thread panics while holding the lock, later lock() calls return Err. Decide whether the invariant is broken (propagate) or recoverable (use the guard anyway), rather than reaching for unwrap reflexively.
  • Cow is the right return type for a function that sometimes allocates and often does not, such as normalisation, escaping or default substitution.
  • For read-mostly shared configuration, prefer Arc<T> with no lock at all, or arc_swap, over a mutex that is taken on every read.

Cycles and Weak

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Parent {
    name: String,
    // children own nothing back: Weak breaks the cycle
    children: RefCell<Vec<Rc<Child>>>,
}

#[derive(Debug)]
struct Child {
    name: String,
    parent: Weak<Parent>,      // observing, not owning
}

fn main() {
    let parent = Rc::new(Parent { name: "p".into(), children: RefCell::new(vec![]) });
    let child = Rc::new(Child { name: "c".into(), parent: Rc::downgrade(&parent) });

    parent.children.borrow_mut().push(Rc::clone(&child));

    let p = child.parent.upgrade();       // Option<Rc<Parent>>
    match p {
        Some(p) => println!("child belongs to {}", p.name),
        None => println!("parent is gone"),
    }

    // counts: parent 1 strong (the local binding), child 2 strong
    println!(
        "parent strong={} weak={}",
        Rc::strong_count(&parent),
        Rc::weak_count(&parent)
    );
}
  • Two Rc values pointing at each other form a cycle, and the strong count never reaches zero, so the memory is never freed. This is the one leak Rust's ownership model cannot prevent on its own.
  • Weak holds a non-owning reference. It does not keep the value alive, and upgrade() returns None once the strong count hits zero — the safe way to express a back pointer.
  • The rule of thumb: a tree owns its children with Rc, and the child observes its parent with Weak. Sibling links, caches and observers are all Weak.
  • Weak is also the basis of a self-referential structure that must not keep itself alive, such as an internal cache keyed by nodes.
  • In async code the same problem appears as a task holding a handle to itself; there the fix is to drop the handle explicitly when the task finishes.
⚠️
Prefer restructuring over RefCell when a borrow panic appears. A run-time borrow panic in production is strictly worse than a compile error, and the usual cause is a design where one owner could hold the data plus explicit method calls instead of shared mutable access.

FAQ

Rc or Arc?
Rc unless the value crosses a thread boundary. The atomic increments in Arc are measurably slower, and the compiler will tell you when Rc is not enough because Rc is not Send.
Why does my program panic on borrow_mut?
Something else is already borrowing the same RefCell. Look for a guard held across a function call, or a closure that borrows while calling a method on the same value. Drop the first guard before taking the second.

Traits, generics and lifetimes Concurrency and async Rust

Last refreshed 2026-09-18.