Rc, RefCell and shared mutation

One owner is the rule that makes Rust easy to reason about, and a few data structures need more than one: a graph node with two parents, a configuration read by several objects, a list of callbacks that each hold the same buffer. Rc allows several owners and counts them; RefCell allows mutation through a shared value and checks the borrow at run time.

This lesson clones a shared vector and watches the count, mutates through Rc<RefCell<T>>, makes a borrow fail on purpose, and ends with the compiler refusing an assignment through a plain Rc. The two types are the safe half of what a C++ programmer reaches for when a shared_ptr is not enough.

A value with two owners

Rc::new puts a value on the heap with a count next to it, and Rc::clone increments the count and returns a new handle. The clone is cheap: one pointer and one increment, never a copy of the value.

The program shares a vector between two bindings and prints the count before and after one handle is dropped. The vector itself is read through the second handle.

use std::rc::Rc; fn main() { let shared = Rc::new(vec![1, 2, 3]); let copy = Rc::clone(&shared); println!("{}", Rc::strong_count(&shared)); println!("{}", copy.len()); drop(copy); println!("{}", Rc::strong_count(&shared)); }

rustc --edition 2024 count.rs
./count

2
3
1

The count goes to two and back to one, and the vector is dropped when the last handle goes. There is a second counter, weak_count, which the next lesson uses for references that do not keep the value alive. The ownership lesson explains the single owner that Rc relaxes.

Mutation behind a shared value

Rc hands out shared references, and a shared reference cannot be written through. RefCell is the container that allows it: the borrow checker sees one shared reference to the cell, and the cell counts its own borrows at run time.

The program keeps a log in an Rc<RefCell<Vec<&str>>> and appends through two handles. Both pushes reach the same vector.

use std::cell::RefCell; use std::rc::Rc; fn main() { let log = Rc::new(RefCell::new(Vec::new())); let first = Rc::clone(&log); first.borrow_mut().push("one"); log.borrow_mut().push("two"); println!("{:?}", log.borrow()); println!("{}", Rc::strong_count(&log)); }

rustc --edition 2024 shared.rs
./shared

["one", "two"]
2

borrow_mut returns a guard, and the guard is dropped at the end of the statement, which is where the borrow ends. Keeping the guard in a variable would extend the borrow to the end of its scope, exactly like a &mut. The lesson on mutable borrows is the compile-time form of the same rule.

A borrow that can be refused

A second mutable borrow of one RefCell is a panic, because the check that the compiler used to do now happens while the program runs. There is also a checked form: try_borrow_mut returns a Result and the program decides what to do.

The program holds a shared borrow and asks for a mutable one. The request is refused, the message is printed, and the shared borrow is still readable.

use std::cell::RefCell; fn main() { let cell = RefCell::new(1); let first = cell.borrow(); match cell.try_borrow_mut() { Ok(mut value) => *value += 1, Err(error) => println!("refused: {error}"), } println!("{}", *first); }

rustc --edition 2024 refused.rs
./refused

refused: RefCell already borrowed
1

The unchecked borrow_mut would have panicked with already borrowed: BorrowMutError at the same line. A program that can see the conflict coming uses the checked form, and one whose logic guarantees a single borrow uses the panicking one and lets the test suite find the bug.

A shared field in a struct

A struct field of type Rc<Config> is how two objects read one setting without copying it. Each constructor takes a clone of the handle, and the last drop frees the config.

The program makes one config and one worker that holds it. Both read the same name, and the count is two because the local binding still owns a handle.

use std::rc::Rc; struct Config { name: &'static str, } struct Worker { config: Rc<Config>, } fn main() { let config = Rc::new(Config { name: "batch" }); let worker = Worker { config: Rc::clone(&config) }; println!("{} {}", config.name, worker.config.name); println!("{}", Rc::strong_count(&config)); }

rustc --edition 2024 field.rs
./field

batch batch
2

The field with a capital C is the type and the lowercase one is the value, which is the shorthand the lesson on structs introduces. A configuration that changes at run time would be Rc<RefCell<Config>>, and every reader would borrow it.

The runtime check is not free: every borrow reads and writes two counters, and the guard keeps them correct until it drops. That cost is why the cell is used at a boundary that a borrow cannot express, and not inside a loop that walks a vector. When the count is known to be one, Rc::get_mut hands out a mutable reference without the cell at all.

An assignment through Rc

Rc implements Deref but not DerefMut, because handing out a mutable reference would break the other owners. The compiler reports E0594 and names the missing trait.

The program adds one to the value inside an Rc<i32>. The fix is a RefCell inside the Rc, or an Rc::get_mut call when the count is one.

use std::rc::Rc; fn main() { let value = Rc::new(5); *value += 1; println!("{value}"); }

rustc --edition 2024 rc_err.rs

error[E0594]: cannot assign to data in an `Rc`
--> rc_err.rs:5:5
|
5 | *value += 1;
| ^^^^^^^^^^^ cannot assign
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<i32>`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0594`.

The help line is the whole design: the trait that would allow the write is missing on purpose. A Box does implement it, because a box has one owner, which is the difference between the two types. The lesson on Arc and Weak takes the same idea to several threads.

Exercises

  1. Share a counter between two structs with Rc<RefCell<i32>> and increment it from both.
  2. Print the strong count after each clone in a loop and explain the last number.
  3. Replace a panicking borrow_mut with try_borrow_mut and print the error.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does Rc::clone copy?

    Answer

    A pointer, and it increments the count

    The clone is one pointer and one increment, which is why it is cheap.

  2. What does RefCell check at run time?

    Answer

    That no mutable borrow overlaps a shared one

    The cell counts its borrows and panics or returns an error on a conflict.

  3. Why does Rc<T> not implement DerefMut?

    Answer

    A mutable reference would break the other owners

    Several owners share the value, so a write through one of them cannot be allowed.

  4. What happens when the last Rc handle is dropped?

    Answer

    The value is dropped

    The count reaches zero and the allocation is released.

  5. What does this program print? use std::rc::Rc; fn main() { let value = Rc::new(10); let copy = Rc::clone(&value); println!("{} {}", Rc::strong_count(&value), *copy + 1); }

    Answer

    2 11

    Two handles exist and the copy reads the same ten.

  6. What does this program print? use std::cell::RefCell; fn main() { let cell = RefCell::new(5); *cell.borrow_mut() += 2; println!("{}", cell.borrow()); }

    Answer

    7

    The guard writes through the cell, and the next borrow reads the new value.

  7. What does this program print? use std::cell::RefCell; fn main() { let cell = RefCell::new(1); let first = cell.borrow(); match cell.try_borrow_mut() { Ok(mut value) => *value += 1, Err(_) => println!("refused"), } println!("{}", *first); }

    Answer

    refused / 1

    The shared borrow is still alive, so the mutable one is refused and the value stays one.

  8. What does this program print? use std::rc::Rc; fn main() { let value = Rc::new(String::from("heap")); let copy = Rc::clone(&value); drop(value); println!("{}", copy.len()); }

    Answer

    4

    The copy keeps the count at one, so the string is still alive.

  9. What is the first line of the compiler output for this program? use std::rc::Rc; fn main() { let value = Rc::new(5); *value += 1; println!("{value}"); }

    Answer

    error[E0594]: cannot assign to data in an Rc

    The shared owner does not provide a mutable dereference.

  10. What does this program print? use std::cell::RefCell; fn main() { let cell = RefCell::new(1); let first = cell.borrow(); println!("{}", cell.try_borrow_mut().is_err()); println!("{}", *first); }

    Answer

    true / 1

    The shared borrow is alive, so the mutable request is refused and the value is unchanged.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image