Getting refrences from Rc<RefCell<Vec<T>>>

So I am dealing with some variables that are shared and must have mutable access from several places that won't conflict at runtime. These variables are mostly a wrapper around some vectors of generic elements.

To that effect, I have wrapped my structure with an Rc and RefCell. The type essentially looks like this:

type Shell<T> = Rc<RefCell<Vec<T>>>;

Because of how the program works, I have several of these collected in an array with a known size.

So the final thing is more like this:

type ArrShell<const N: usize, T>= [Shell<T>; N];

There is this function that only needs the last element of every item in the ArrShell. I'd like it to take a form that is easy to debug later on, meaning that the author of that function only has to see that he has received an array of references to the underlying type T.

How can I do that?? I have tried the following:

use std::rc::Rc;
use std::cell::RefCell;

type Shell<T> = Rc<RefCell<Vec<T>>>;
type ArrShell<const N: usize, T>= [Shell<T>; N];

fn ideal_case(_a: &[&Unclonable; 2]){
    todo!()
}

fn also_acceptable<'a, T: AsRef<[&'a Unclonable]>>(_a: T){
    todo!()
}

struct Unclonable{
    a: f64
}

fn main() {
    let a = vec![Unclonable{a: 5.0}, Unclonable{a: 5.0}, Unclonable{a: 5.0}];
    let b = vec![Unclonable{a: 5.0}, Unclonable{a: 5.0}, Unclonable{a: 5.0}];
    let arr_of_shells: ArrShell<2, _> = [Rc::new(RefCell::new(a)), Rc::new(RefCell::new(b))];
    let ref_vec = arr_of_shells.map(|e| e.borrow().last().unwrap());
    ideal_case(&ref_vec);
    also_acceptable(&ref_vec);
}

but I am logically getting an error in the map, as I am trying to return an internal reference to the closure.

 let ref_vec = arr_of_shells.map(|e| e.borrow().last().unwrap());
   |                                         ----------^^^^^^^^^^^^^^^^
   |                                         |
   |                                         returns a value referencing data owned by the current function
   |                                         temporary value created here

Is there any way to do what I want?
I feel there should be a way, but I haven't been able to make it work.

.borrow() doesn't give you a reference. It returns a Ref guard, which ensures that the access stays limited and ends as soon as Ref is dropped.

When you call .borrow() and don't store the Ref in some more permanent location, your permission to use the cell ends immediately at the end of this expression/scope.

This can't work:

.map(|e| e.borrow().last().unwrap())

because it means:

.map(|e| {
   let tmp: Ref<_> = e.borrow();
   let reference_to_tmp: &T = tmp.last().unwrap();
   drop(tmp);
   return reference_to_tmp; // ooops, use after tmp expired!
})

You would have to store the Ref somewhere first, and then use its contents only within the same scope (the same function call) as the place that stores it.


A common pattern is to use Rc for the elements you return:

Rc<RefCell<Vec<Rc<T>>>>

This allows you to do:

.map(|e| e.borrow().last().unwrap().clone())

and that last .clone() then gives you a fresh Rc<T> which you can keep for as long as you like, even after RefCell stops giving you temporary access.


Alternatively, there's Ref::map, so instead of &T you can return Ref<T>. Ref's map allows you to change Ref<Vec<T>> that you have into Ref<T>. Returning a Ref wrapper keeps the "lock" on the RefCell alive, so it's limited not to the scope of .borrow() call, but a bit larger scope of the RefCell itself.

4 Likes

The latter option could look like

    let ref_vec: Vec<Ref<'_, Unclonable>> = arr_of_shells
        .iter()
        .map(|e| Ref::map(e.borrow(), |v| v.last().unwrap()))
        .collect();
2 Likes

Thank you both so much!!!

To offer a different (not mechanistic, but bird's eye view) perspective about the problem: how would you expect RefCell to uphold its safety guarantees if it were at all possible to get a reference to the wrapped value that outlived the cell?

That wouldn't quite make sense. RefCell effectively performs reference counting of the outstanding borrows. It only allows mutation if there is no other outstanding loan (either mutable or immutable). The way it can do this is to return these guard types, Ref and RefMut, which are RAII guards: they call back to the RefCell when they go out of scope, so that the appropriate internal counter can be decremented. In addition, they limit the validity of the returned reference to the lifetime of the Ref/RefMut guards, so that the non-existence of an outstanding borrow is ensured once the Ref/RefMut is destroyed.

If the RefCell or the Ref/RefMuts allowed you to get a reference that outlives the guards, then RefCell would be unsound. You could then do this:

let cell = RefCell::new(value);
let immut_ref_guard = cell.borrow();
let immut_ref = &*immut_ref_guard;

drop(immut_ref_guard);
// at this point there's no outstanding loan according to the
// RefCell, because the `immut_ref_guard` has been dropped.
// This is **false:** `immut_ref` still points into the cell!

let mut mut_ref_guard = cell.borrow_mut();
let mut_ref = &mut *mut_ref_guard;
// BOOM: Undefined Behavior, aliased &mut T to the cell's value!

For example, run this Playground under Miri to see it report UB.

1 Like

Thanks!!