How do I get a reference from a Cell?

I'm practicing using Cell to change things internally, here's my code:

pub struct Metadata {
    pub size: usize,
    children: Cell<FxHashMap<usize, Metadata>>,
}

impl Metadata {
    pub fn new() -> Self {
        Self {
            size: 0,
            children: Cell::new(FxHashMap::default()),
        }
    }
}

impl Index<usize> for Metadata {
    type Output = Metadata;

    fn index(&self, index: usize) -> &Self::Output {
        let mut map = self.children.take();
        if !map.contains_key(&index) {
            map.insert(index, Metadata::new());
            self.children.replace(map);
        }
        self.children.get(&index) // won't compile, how to get a reference of the map?
    }
}

What I want is that if the map do not have the element, I'll create one first (just like C++), and I don't want to change the &self parameter to &mut self, that's why I choose the Cell, but the get() function of a cell returns a copy, not a reference.

So how can I do this?

What you are looking for is a RefCell instead of a Cell.

A Cell ensures the aliasing invariants of Rust by ensuring there are never any aliases (references) to the value. Only reads and writes.

A RefCell does runtime tracking of read and write accesses.

3 Likes

You can't. I talk about why in this blog post.

2 Likes

Can I understand that Cell is designed for simple data structures (just need read and write), not for complicated ones?

Well, it is designed for cases that need only the following operations:

  1. set
  2. get
  3. swap

For non-Copy types, only set and swap are available.

2 Likes

So in my case, any safe design to accomplish my goal while avoiding runtime borrow checking?

Not if you wish to mutate the contents of self while under an & reference.

I got another problem using RefCell:

impl Index<usize> for Metadata {
    type Output = Metadata;

    fn index(&self, index: usize) -> &Self::Output {
        if !self.children.borrow_mut().contains_key(&index) {
            self.children.borrow_mut().insert(index, Metadata::new());
        }
        self.children.borrow().get(&index).unwrap()
    }
}

it seems that the returned reference's lifetime is bound to the Ref<'_, T> returned by borrow, and I got lifetime error:

26 |         self.children.borrow().get(&index).unwrap()
   |         ----------------------^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         returns a value referencing data owned by the current function
   |         temporary value created here

I'm not sure if it's good to force cast the lifetime here, is there any safe way?

You can't implement Index if the collection is behind a cell because the cell would allow you to change the collection while references into it exist.

1 Like

Or to explain the latest error more directly, your borrow must expire when the Ref drops because that's the run-time tracking in action. (And the Index trait doesn't allow you to return a Ref<'_, _>.)

You could do something like this:

impl Metadata {
    // Hiding the implementation details by returning an opaque type
    // You can return `Ref<'_, Metadata>` if you don't want/need to hide it
    pub fn get(&self, index: usize) -> impl Deref<Target=Metadata> + '_ {
        if !self.children.borrow().contains_key(&index) {
            self.children.borrow_mut().insert(index, Metadata::new());
        }

        // This is a change from your code        
        Ref::map(self.children.borrow(), |hm| hm.get(&index).unwrap())
    }
    
    // For users that just have to have a reference
    pub fn run<F, R>(&self, index: usize, f: F) -> R 
    where
        F: FnOnce(&Metadata) -> R,
    {
        let borrow = self.get(index);
        f(&*borrow)
    }
}
1 Like

I'm not quite understand, since I can get a reference from a collection directly, why can't I get it behind a RefCell? Won't the borrow rules prevent me change the collection if I'm holding a immutable reference?

Thanks for helping, If I'm a user, what should I pass as a f: F?

Because access to the contents of a RefCell can only happen while the Ref or RefMut object returned by borrow() or borrow_mut() still exists. This is because the RefCell keeps track of those calls with a counter, and that counter is decremented in the destructor of the Ref and RefMut objects. It wouldn't be safe to obtain a &T from the Ref<T>, destroy the Ref<T> and then use the &T because the RefCell's counter just got decremented, so it is no longer able to keep track of the &T.

This is what's happening in your error. The Ref object is destroyed when you return from the function, but you are trying to use the &T after it was destroyed by returning it.

2 Likes

So simply speaking no Ref (RefMut), not borrow rules checking, right? Because the checking logic is implemented in Ref, not some compiler inserted code?

I mean, the borrow rules still apply "normally". It's just that borrow_mut() takes &self, so the compiler will not require you to have mutable access to the RefCell to call it, even though the returned RefMut will let you obtain a &mut T to the inner value.

A function or closure that takes a &Metadata as an argument.

fn some_func(s: String, _m: &Metadata) -> usize {
    s.len()
}

fn main() {
    let metadata = Metadata::new();
    let my_string = "Hello, World".to_string();
    let len = metadata.run(17, |m| some_func(my_string, m));
    println!("{}", len);
}
1 Like

Thanks guys, I got a feeling on how "RefCell" works now, I misunderstood what is "inner changeability", It does not mean I can leak references outside a cell.

I'll check the source code to study further.

What you CAN do is get a Rc<RefCell<T>> from anywhere. Rust has many kinds of pointers, I tend to think of Rc<RefCell<T>> as a "First Class" pointer, the kind of pointer you are used to in other languages. Rust's "Second class" references (maybe "temporary" is a better word) are great, no dynamic checks when they are used, but they have lifetime restrictions on them, and cannot be used in every situation where you want a pointer.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.