Skip to content

← Smart Pointers and Interior Mutability step 7 of 26

Hard Primitives

`Rc::make_mut`: copy-on-write shared data

Edit a shared vector, take a snapshot part-way through, and report whether the two still share one allocation.

pub fn cow_edits(
    initial: Vec<i32>,
    ops: Vec<(usize, i32)>,
    snapshot_after: usize,
) -> (Vec<i32>, Vec<i32>, bool)

Put initial in an Rc<Vec<i32>>. Each op is (index, delta) and adds delta to the element at index. An op whose index is out of range does nothing at all — and, importantly, must not cause a copy either.

After exactly min(snapshot_after, ops.len()) ops have been applied, take a shared handle — a second Rc onto the same allocation, not a copy of the data. Keep it alive to the end.

Return (the final vector, the snapshot's vector, Rc::ptr_eq(final, snapshot)).

The tool almost nobody finds

You have just been told that Rc hands out &T and only &T. The obvious conclusion — “so to mutate shared data I need RefCell“ — is the single most over-applied idea in this track, and it skips a type that is better for the most common case.

Rc::make_mut(&mut rc) -> &mut T      // where T: Clone

It does exactly one clever thing:

  • if the strong count is 1, hand out &mut T directly — free, no copy;
  • if the count is greater than 1, clone the value into a fresh allocation, point this handle at the new one, and hand out &mut T to that.

This is copy-on-write, the same idea behind fork()‘s page tables and every persistent data structure you have used. The name for it in Rust is Cow, and Rc::make_mut is the version that works when the sharing is by refcount.

For mostly-shared, rarely-mutated data it dominates RefCell on every axis you care about: no runtime borrow flags, no branch on every access, and — crucially — it cannot panic. A BorrowMutError is a runtime failure mode you have chosen to accept; make_mut simply has no such mode.

The surprise, and why this problem exists

With a and b sharing a Rc<Vec<i32>> holding [1,2,3]:

Rc::make_mut(&mut a).push(4);
// a == [1, 2, 3, 4]
// b == [1, 2, 3]           <- b did NOT see the change

Almost everyone expects b to change. It does not, and it never will — make_mut broke the sharing. Afterwards a and b each have a strong count of 1 and Rc::ptr_eq(&a, &b) is false. The clone was invisible: no syntax marked it, no error announced it, and a println! of the values would not have told you it happened.

That is what the returned bool is for. Making the invisible clone observable is the entire pedagogical trick here, and it is the difference between believing you understand make_mut and actually understanding it.

When to use it and when not to

Use it when: the data is Clone, sharing is the common case, mutation is rare, and each mutator owns its own handle.

Do not use it when:

  • The clone is expensive and the data is heavily shared. Note the cruelty of the cost model: make_mut copies the whole value exactly when the count is high — that is, exactly when the data is most shared, which for a large Vec is exactly when you can least afford it.
  • You need several holders to see each other’s writes. They cannot. The moment one writes, it forks. Observer graphs, shared caches and callback registries genuinely need RefCell, and that is what it is for.
  • You only have &Rc<T>. make_mut requires &mut Rc<T> — an exclusive borrow of your handle, not of the data. Any design that hands out shared Rcs from a central table cannot call it.

Getting the ptr_eq case right

Two details decide the hidden cases.

Call make_mut only when you are actually going to write. If you call it before checking the index, an out-of-range op forks the allocation for nothing, and the shared flag comes back false where the tests expect true.

And the snapshot must be Rc::clone(&data) — a handle. If you reach for data.as_ref().clone() or (*data).clone() you get a copy of the vector, which is a different allocation from the start, which makes ptr_eq false everywhere and defeats the whole exercise.

Loading visualization…