Rc and mutability

The book says that Rc doesn't allow you to borrow the inner data mutably.
Rc is for single-threaded programs only and it doesn't make sense to me why should we use RefCell to mutate the inner data?
when it's single-threaded we're sure the inner data can't be accessed more than once at the same time.
it just doesn't make sense to me can somebody explain why RefCell is needed?

If that was allowed, then this would compile:

use std::rc::Rc;

fn main() {
    let my_vec = vec![10];
    
    let rc1 = Rc::new(my_vec);
    let rc2 = rc1.clone();
    
    let ref_to_10 = &rc1[0];
    rc2.clear();
    
    println!("{}", ref_to_10);
}

However this code is incorrect as the ref_to_10 reference would point to a destroyed value due to the clear call.

2 Likes

You are the victim of a common misconception. Shared mutability is not only a problem in multi-threaded code. Shared mutability is unsound even in single-threaded code. Since Rc is used exactly for sharing ownership, allowing mutable access through one Rc would also access mutable access to the same object through a copy of that Rc, so something needs to track at runtime that the Rc is in fact unique. (RefCell can do that, but so does Rc::get_mut().)

2 Likes

Rust's borrowing rules are not just to stop data races, they also make sure pointers are always valid, for example to ensure a pointer(reference) does not point to memory that has been de-allocated.

In single-threaded contexts Rustʼs differentiation of &mut T vs &T is still important. A typical example why is iterator invalidation. If you create some vec.iter() iterator, then that iterator holds pointers into the Vec's memory. Modifying (and this potentially growing) the vec can invalidate this memory, so it's important that the self: &mut Vec<T> argument for something like Vec::push has exclusive access to the vector, even in single-threaded contexts, so you cannot keep using the same iterator after a call to .push(...).

A Rc is by nature a shared reference, hence a (&mut Rc<T>) -> &mut T method cannot exist without some kind of run-time checking ensuring the returned reference is truly exclusive. RefCell does this with an internal flag that's modified when you access its contents; Rc itself actually does allow mutable access, too, but only when there's no other clone of the same Rc around, via Rc::get_mut. The RefCell approach uses a runtime flag to track your access pattern of mutable and immutable borrows if it's contents and prevent any violation of &mut T exclusivity guarantees; and this flag has memory and time overhead, so it's not something that Rc itself includes by default. RefCell also complicates the API for immutable access to the contained value by forcing you to go through a handle type std::cell::Ref that makes sure the flag of the RefCell is updated accordingly once the immutable access ends and the handle is dropped. You wouldn't want this limitation for all Rcs by default.

5 Likes

You might be interested in this classic post: The Problem With Single-threaded Shared Mutability - In Pursuit of Laziness

Also, for the simple cases that's exactly what Cell in std::cell - Rust is for -- if you just want a simple i32 and to be able to read and write it from one thread like you would in something like Java, just use a Cell<i32> and you can.

3 Likes