Skip to content

11. Box, Rc, Arc and RefCell

Full example: examples/l11_smart_pointers.rscargo run --example l11_smart_pointers.

In C# and Java, every object of a class lives on the heap, any number of variables can refer to it, any of them can modify it, and the garbage collector frees it when nobody refers to it anymore.

Rust splits that bundle into separate, opt-in pieces:

You need Rust Cost
a value on the heap with one owner Box<T> one allocation
several owners of a value, one thread Rc<T> a reference count
several owners across threads Arc<T> an atomic reference count
to mutate something that is shared RefCell<T> (one thread), Mutex<T> (threads) a borrow check at runtime

A C# class reference is roughly Rc<RefCell<T>> (or Arc<Mutex<T>> when threads are involved). Rust makes you ask for each capability, and most code needs none of them.

They are called smart pointers because they own their data and implement Deref. calls methods on the value inside — and Drop — cleanup happens automatically.

Box::new(value) moves the value to the heap; the Box itself is a pointer-sized owner. When the box goes out of scope, the heap memory is freed.

You already met Box<dyn Trait> in lesson 7. The other classic use is a recursive type. In C#, class Expr { Expr Left; } is fine because Left is a reference. In Rust, an enum stores its fields inline, so a type containing itself would be infinitely large:

enum Expr {
Num(f64),
Add(Expr, Expr),
}
error[E0072]: recursive type `Expr` has infinite size
--> e11_recursive.rs:1:1
|
1 | enum Expr {
| ^^^^^^^^^
2 | Num(f64),
3 | Add(Expr, Expr),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
3 | Add(Box<Expr>, Expr),
| ++++ +

A Box has a fixed size, whatever it points to:

#[derive(Debug)]
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
fn eval(expr: &Expr) -> f64 {
match expr {
Expr::Num(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b), // &Box<Expr> is used as &Expr
Expr::Mul(a, b) => eval(a) * eval(b),
}
}
// (2 + 3) * 4
let expr = Expr::Mul(
Box::new(Expr::Add(Box::new(Expr::Num(2.0)), Box::new(Expr::Num(3.0)))),
Box::new(Expr::Num(4.0)),
);
// Mul(Add(Num(2.0), Num(3.0)), Num(4.0)) = 20

Sometimes a value has no single natural owner: several services share one configuration, several nodes of a graph point to the same node. Rc (reference counted) allows that:

use std::rc::Rc;
struct Config { env: String }
struct Service { name: &'static str, config: Rc<Config> }
let config = Rc::new(Config { env: "prod".into() });
let api = Service { name: "api", config: Rc::clone(&config) };
let worker = Service { name: "worker", config: Rc::clone(&config) };
println!("strong count = {}", Rc::strong_count(&config)); // 3
drop(api);
println!("strong count = {}", Rc::strong_count(&config)); // 2

Rc::clone does not copy the Config: it increments a counter and returns another pointer to the same value. When the last Rc is dropped, the counter reaches zero and the value is freed — deterministic, unlike a garbage collector. The convention of writing Rc::clone(&x) rather than x.clone() makes the cheap pointer copy visible in code review.

Shared means read-only. Two owners mutating the same value is precisely what the borrow rules forbid:

let shared = Rc::new(vec![1, 2]);
let other = Rc::clone(&shared);
other.push(3);
error[E0596]: cannot borrow data in an `Rc` as mutable
--> e11_rc_mut.rs:6:5
|
6 | other.push(3);
| ^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Vec<i32>>`

RefCell<T>: borrow rules checked at runtime

Section titled “RefCell<T>: borrow rules checked at runtime”

Interior mutability lets you mutate through a shared reference. RefCell keeps the rule from lesson 4 — many readers or one writer — but checks it when the program runs instead of when it compiles:

use std::cell::RefCell;
#[derive(Debug)]
struct Account { balance: i64 }
let account = Rc::new(RefCell::new(Account { balance: 100 }));
let alice = Rc::clone(&account);
let bob = Rc::clone(&account);
alice.borrow_mut().balance -= 30; // borrow_mut() -> RefMut<Account>, like &mut
bob.borrow_mut().balance += 5;
println!("{:?}", account.borrow()); // borrow() -> Ref<Account>, like &
// Account { balance: 75 }

Break the rule and the program panics:

let log = RefCell::new(Vec::new());
let reader = log.borrow();
log.borrow_mut().push("boom");
println!("{}", reader.len());
thread 'main' (…) panicked at p11_refcell.rs:6:9:
RefCell already borrowed

try_borrow and try_borrow_mut return a Result instead of panicking:

let reading = account.borrow();
account.try_borrow_mut().is_err() // true: refused while `reading` is alive

For Copy values such as counters and flags, Cell<T> is simpler: it never lends a reference, it just gets and sets.

use std::cell::Cell;
let hits = Cell::new(0);
hits.set(hits.get() + 1);

Reference counting cannot free a cycle: if a points to b and b points to a, both counts stay above zero forever. A garbage collector handles this; Rc does not.

struct Node {
name: &'static str,
next: RefCell<Option<Rc<Node>>>,
}
// with a Drop impl that prints "drop {name}"
{
let a = Rc::new(Node { name: "a", next: RefCell::new(None) });
let b = Rc::new(Node { name: "b", next: RefCell::new(Some(Rc::clone(&a))) });
*a.next.borrow_mut() = Some(Rc::clone(&b));
println!("a strong = {}, b strong = {}", Rc::strong_count(&a), Rc::strong_count(&b));
}
println!("end of scope: nothing dropped");
a strong = 2, b strong = 2
end of scope: nothing dropped

The drop messages never appear: the memory leaked. This is memory-safe — no dangling pointer — but still a bug.

The fix is a Weak<T> pointer for one direction of the link. A Weak does not keep the value alive; upgrade() returns Some(Rc<T>) if the value still exists and None otherwise — like C#’s WeakReference<T>.TryGetTarget or Java’s WeakReference.get(). The usual design: parents own their children (Rc), children point back to their parent (Weak).

struct TreeNode {
name: String,
parent: RefCell<Weak<TreeNode>>,
children: RefCell<Vec<Rc<TreeNode>>>,
}
*leaf.parent.borrow_mut() = Rc::downgrade(&root);
root.children.borrow_mut().push(Rc::clone(&leaf));
let parent_name = leaf.parent.borrow().upgrade().map(|p| p.name.clone());
leaf's parent: Some("root"), children of root: 1
root strong = 1, weak = 1
drop root
drop leaf
tree scope ended

Rc updates its counter without synchronisation, so the compiler refuses to send it to another thread:

let config = Rc::new(String::from("prod"));
let copy = Rc::clone(&config);
let handle = std::thread::spawn(move || println!("{copy}"));
error[E0277]: `Rc<String>` cannot be sent between threads safely
--> e11_rc_thread.rs:7:32
|
7 | let handle = thread::spawn(move || println!("{copy}"));
| ------------- -------^^^^^^^^^^^^^^^^^^^
| | |
| | `Rc<String>` cannot be sent between threads safely
| | within this `{closure@e11_rc_thread.rs:7:32: 7:39}`
| required by a bound introduced by this call
|
= help: within `{closure@e11_rc_thread.rs:7:32: 7:39}`, the trait `Send` is not implemented for `Rc<String>`

Arc (atomically reference counted) has the same API with a thread-safe counter:

use std::sync::Arc;
let shared = Arc::new(vec![1, 2, 3]);
let for_thread = Arc::clone(&shared);
let sum = std::thread::spawn(move || for_thread.iter().sum::<i32>()).join().unwrap();
// sum computed on another thread: 6, strong count back to 1

Why not always use Arc? Atomic operations cost more, and Rc documents that a value stays on one thread. The thread-safe partner of RefCell is Mutex or RwLocklesson 12.

Question Answer
One owner, but the value must be on the heap (recursive type, dyn Trait, large value to move cheaply)? Box<T>
Several owners, read-only, one thread? Rc<T>
Several owners, read-only, several threads? Arc<T>
Several owners that mutate, one thread? Rc<RefCell<T>>
Several owners that mutate, several threads? Arc<Mutex<T>> or Arc<RwLock<T>>
A back-reference or a cache that must not keep things alive? Weak<T>
  • C# and Java give every object heap allocation, sharing and mutation at once; Rust makes each one explicit.
  • Box<T> puts one owned value on the heap: recursive types and trait objects.
  • Rc<T> and Arc<T> count owners and free the value when the last one goes; Rc::clone copies a pointer, not the data.
  • RefCell<T> moves the borrow check to runtime: violations panic instead of failing to compile.
  • Reference counting leaks cycles; break them with Weak<T>.
  • Rc is not Send; use Arc across threads.
  1. Add a Neg(Box<Expr>) variant to Expr, update eval, and write fn show(e: &Expr) -> String so that (2 + 3) * -4 prints as ((2 + 3) * -4).
Solution
enum Expr {
Num(f64),
Neg(Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
use Expr::*;
fn eval(e: &Expr) -> f64 {
match e {
Num(n) => *n,
Neg(a) => -eval(a),
Add(a, b) => eval(a) + eval(b),
Mul(a, b) => eval(a) * eval(b),
}
}
fn show(e: &Expr) -> String {
match e {
Num(n) => n.to_string(),
Neg(a) => format!("-{}", show(a)),
Add(a, b) => format!("({} + {})", show(a), show(b)),
Mul(a, b) => format!("({} * {})", show(a), show(b)),
}
}
let e = Mul(Box::new(Add(Box::new(Num(2.0)), Box::new(Num(3.0)))), Box::new(Neg(Box::new(Num(4.0)))));
assert_eq!(show(&e), "((2 + 3) * -4)");
assert_eq!(eval(&e), -20.0);

use Expr::*; brings the variants into scope. f64’s to_string() prints 2.0 as 2.

  1. A Cart and a Payment must both append lines to the same log. Model it with type Log = Rc<RefCell<Vec<String>>>. Then predict what this does:
let lines = Rc::new(RefCell::new(vec![String::from("start")]));
for line in lines.borrow().iter() {
lines.borrow_mut().push(format!("seen {line}"));
}
Solution
use std::cell::RefCell;
use std::rc::Rc;
type Log = Rc<RefCell<Vec<String>>>;
struct Cart { log: Log }
struct Payment { log: Log }
impl Cart {
fn add(&self, item: &str) {
self.log.borrow_mut().push(format!("cart: added {item}"));
}
}
impl Payment {
fn pay(&self, amount: u32) {
self.log.borrow_mut().push(format!("payment: {amount}"));
}
}
let log: Log = Rc::new(RefCell::new(Vec::new()));
let cart = Cart { log: Rc::clone(&log) };
let payment = Payment { log: Rc::clone(&log) };
cart.add("book");
payment.pay(40);
assert_eq!(*log.borrow(), ["cart: added book", "payment: 40"]);

Note that add and pay take &self, not &mut self: the mutation is hidden inside the RefCell.

The loop compiles but panics with RefCell already borrowed: the iterator holds a borrow() for the whole loop, and borrow_mut() inside it is refused. It is lesson 4’s “push while iterating” error, moved from compile time to runtime. Collect the new lines first, then push them.

  1. In the leaking Node example, a points to b and b points to a. Change the design so both nodes are freed at the end of the scope, and check the strong counts.
Solution

Keep next as a strong link and make the backward link Weak:

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: &'static str,
next: RefCell<Option<Rc<Node>>>,
prev: RefCell<Weak<Node>>,
}
{
let a = Rc::new(Node { name: "a", next: RefCell::new(None), prev: RefCell::new(Weak::new()) });
let b = Rc::new(Node { name: "b", next: RefCell::new(None), prev: RefCell::new(Weak::new()) });
*a.next.borrow_mut() = Some(Rc::clone(&b));
*b.prev.borrow_mut() = Rc::downgrade(&a);
println!("a strong = {}, b strong = {}", Rc::strong_count(&a), Rc::strong_count(&b));
}
println!("end of scope");
a strong = 1, b strong = 2
drop a
drop b
end of scope

b is dropped first (its count goes from 2 to 1), then a (1 to 0, so it is freed), and freeing a drops its next, which frees b.