RefCell2, a totally broken RefCell


#1

UPDATE: this is totally broken, unsound, etc. Don’t use it.

std::cell::Ref has a map function that allows borrowing something that’s reachable from the old value. It doesn’t let you map into other things that share the same lifetime, since the new value needs to be a reference. In this reimplementation of RefCell<T>, a Ref is no longer a Ref<T>, but a Ref<&T> such that you can map it into any other type with the same lifetime.

Example:

use refcell2::{RefCell,Ref};
use std::collections::{HashSet,hash_set};

fn get_iter<'a>(set: &'a RefCell<HashSet<i32>>) -> Ref<hash_set::Iter<'a,i32>> {
    Ref::map(set.borrow(),HashSet::iter)
}

let c=RefCell::new(HashSet::new());
c.borrow_mut().insert(10);
assert!(get_iter(&c).by_ref().eq(&[10]));

This is actually a fairly simple change from the original code, basically just adding some &s and &muts here and there. The only drawback in terms of useabilty is that you now have to dereference twice to get to the value.


#2

How can this be safe at all? If Ref doesn’t have a lifetime parameter, it can outlive the original RefCell it was borrowed from.


#3

The new Ref has the same lifetime as the old Ref (not shown).


#4

Oops, I didn’t realize elision could be abused like that, it’s super confusing (cc @nikomatsakis).

So refcell2::Ref<T> is a slimmer (std::cell::Ref<()>, T) - I can see its utility, I just wish it wasn’t necessary.


#5

“If there is exactly one input lifetime, elided or not, that lifetime is assigned to all elided lifetimes in the return values of that function.”

Yes, and in a form that can actually be returned from a function.


#6

Hmm, there might still be unsoundness, does the following compile?

let cell = RefCell::new(0);
let r = *cell.borrow();
let mut m = cell.borrow_mut();
**m = *r;

EDIT: To explain, I believe the safety of Ref<'a ,T> stands in Deref::deref being &self -> &T and the user not being able to get a &'a T. Because there’s nothing stopping you from keeping a &'a T for the whole lifetime of the RefCell, and past the destruction of the Ref<'a, T> you obtained it from.


#7

This is weird. It does compile and run without panics (with let mut m). However, if I explicitly write let r: &i32, it panics at runtime (as it should)…

I have

impl<'b, T: 'b> Deref for Ref<'b, T>

Shouldn’t the lifetime bound take care of the problem there?


#8

That sounds like it’s forcing the result of borrow() to live longer due to the syntactical type. Nothing else makes sense AFAICT.

As for the lifetime bound: no. The only thing it does is prevent &'a &'b T where 'b is shorter than 'a (the two references may be separated by some structural nesting).

What you want is actually impossible in current Rust, even with a helper unsafe trait.
You need to replace the lifetime that refers to the RefCell with one that refers to the Ref on each deref, so that the returned type couldn’t possibly outlive the Ref.

So fn deref<'b>(&'b self) -> &'b Iter<'b, T> is what you want to get instead of exposing Iter<'a, T> - except that requires a HKT Target<'b> associated type:

impl<'a, T: /*unsafe*/ Reborrow> Deref for Ref<'a, T> {
    type Target<'b> = <T as Reborrow<'b>>::Output;
    fn deref<'b>(&'b self) -> &'b Self::Target<'b> {
        Reborrow::reborrow(&self.data)
    }
}

That’s not exactly the best, given limitations in what we could change over time, but that’s more or less what it might end up looking like.


#9

I am not a fan of elision when it hides the existence of lifetime parameters. I have thought about writing an RFC to deprecate that kind of elision – I’d prefer (in that case) something like Ref<'_, ....>, which makes it clear that there is a borrow happening here. But I’m not sure exactly how '_ ought to work (e.g., @wycats had some thoughts that I never fully understood on how to make it most convenient, I think with the idea of a single '_ maybe standing in for a number of parameters).