Skip to content

← Smart Pointers and Interior Mutability step 11 of 26

Medium Primitives

`RefCell<T>`: borrowing checked at runtime

Two functions over a RefCell<Vec<i64>>.

pub fn apply_ops(initial: Vec<i64>, ops: Vec<String>) -> Result<Vec<i64>, String>
pub fn safe_probe(v: Vec<i64>) -> Vec<bool>

apply_ops

Put initial in a RefCell<Vec<i64>> and run the ops in order, stopping at the first error.

op effect error
push <n> append n bad op: <op> if n does not parse
pop remove the last element pop on empty
scale <n> multiply every element by n bad op: <op>
append_sum append the current sum
mirror append the current contents reversed
require <n> check the sum equals n require: want <n>, got <s>
anything else bad op: <op>

So [1,2,3] after mirror is [1,2,3,3,2,1], and after append_sum it is [1,2,3,6].

safe_probe

Put v in a RefCell<Vec<i64>> and return exactly six booleans, produced by this exact script:

  1. take a shared guard a; is the vector empty?
  2. still holding a — does try_borrow() succeed?
  3. still holding a — does try_borrow_mut() succeed?
  4. drop a, take an exclusive guard b; are all elements >= 0?
  5. still holding b — does try_borrow() succeed?
  6. drop b — does try_borrow_mut() succeed?

What RefCell actually does

Cell was sound because it never let a reference to its interior escape. That makes it useless for a Vec: you cannot call .push() on something you cannot get a reference to.

RefCell buys back the references by checking the borrow rules at runtime instead of at compile time. It holds a counter alongside the value:

  • borrow() → checks nothing is exclusively borrowed, increments a shared count, returns a Ref<'_, T> guard;
  • borrow_mut() → checks nothing is borrowed at all, marks it exclusive, returns a RefMut<'_, T> guard;
  • each guard’s Drop restores the counter.

The rule enforced is exactly the compile-time rule — many shared or one exclusive — and the enforcement is a comparison and a branch, one usize per cell. The difference is what happens when you break it. The compiler tells you at build time; RefCell panics at run time, in production, in front of a user.

On Rust 1.95 the two messages are different, and the difference tells you which direction failed:

what failed why message
borrow_mut() something already holds a shared borrow RefCell already borrowed
borrow() something already holds the exclusive borrow RefCell already mutably borrowed

(The Book and most tutorials still quote the older strings already borrowed: BorrowMutError and already mutably borrowed: BorrowError. Those are out of date. The same two current strings are what BorrowMutError/BorrowError Display as.)

The guard controls the lifetime, not the borrow expression

This is the single most useful sentence about RefCell:

The critical section lasts as long as the guard value, not as long as the expression that produced it.

let v = cell.borrow();     // guard lives until end of scope
cell.borrow().len()        // guard is a temporary: dies at end of statement

Both of the starter’s panicking arms are that distinction, misapplied.

data.borrow_mut().push(peek(&data).iter().sum());

A method call evaluates its receiver first. So borrow_mut() runs, the exclusive guard is live as a temporary for the whole statement, and then borrow() runs inside the argument. Panic: RefCell already mutably borrowed.

for x in peek(&data).iter() {
    data.borrow_mut().push(*x);
}

A for loop’s head temporaries live for the entire loop, so the shared guard is held across every iteration. Panic on the first borrow_mut: RefCell already borrowed.

Both fixes are the same move: end the borrow before you start the other one. Read what you need into an owned value, drop the guard, then write. Clippy’s redundant_clone would call that clone wasteful in code where the borrow was not in the way; here it is load-bearing, and knowing the difference is the skill.

E0515: you cannot outlive the guard

The starter opens with the most common RefCell compile error in existence:

fn peek(cell: &RefCell<Vec<i64>>) -> &Vec<i64> {
    &cell.borrow()
}
error[E0515]: cannot return reference to temporary value
  |     &cell.borrow()
  |     ^-------------
  |     ||
  |     |temporary value created here
  |     returns a reference to data owned by the current function

The Ref guard is a local. Returning a reference into it would let the caller hold a &Vec<i64> after the guard had been dropped and the borrow flag cleared — after which someone else could take borrow_mut() and you would have aliasing plus mutation, which is exactly what all of this exists to prevent.

There are two honest fixes. Return an owned clone, or return the guard itself: fn peek(cell: &RefCell<Vec<i64>>) -> Ref<'_, Vec<i64>>. The second is usually what you want — the caller derefs it like a &Vec, and the borrow ends when they drop it. That is the pattern every guard-returning API in std uses, Mutex::lock included.

One more fact worth carrying

RefCell<T> is Send if T is, but it is never Sync — its counter is a plain integer with no synchronisation. So Arc<RefCell<T>> does not compile (E0277), no matter how much you want it to. The cross-thread equivalent is Arc<Mutex<T>>, and clippy has a default-on lint, arc_with_non_send_sync, that catches the attempt at the construction site.

Loading visualization…