Another why does this borrow last so long question


#1

Hi everyone,

I noticed that a user just asked a similar question titled “Why is it borrowed for too long?” and it looks like the answer is not quite applicable to my situation where I have the same problem.

I have written the following code only for purposes of better understanding the borrow checker, vec and RefCell. My problem with the following code is that I don’t understand why the borrow of the vec on line 12 lasts until line 21? The fact that it lasts till line 21 is causing a borrow checker error on Line 17 because I am trying to push to an already immutably borrowed variable.

Code

use std::cell::RefCell;

fn main() {

    let mut vec: Vec<RefCell<i32>> = Vec::new();

    vec.push(RefCell::new(1));

    let _z;

    {
        let ref_cell: &RefCell<i32> = (vec.first().unwrap()); // ===============> Line 12

        _z = ref_cell;
    }

    vec.push(RefCell::new(2)); // ===============> Line 17

    println!("{:?}", vec);

} // ===============> Line 21

Playground

And here is the error

error[E0502]: cannot borrow `vec` as mutable because it is also borrowed as immutable
  --> src/main.rs:17:5
   |
12 |         let ref_cell: &RefCell<i32> = (vec.first().unwrap()); // ===============> Line 12
   |                                        --- immutable borrow occurs here
...
17 |     vec.push(RefCell::new(2)); // ===============> Line 17
   |     ^^^ mutable borrow occurs here
...
21 | } // ===============> Line 21
   | - immutable borrow ends here

error: aborting due to previous error

Any pointers for me?


#2

Given we’re working with references (another name for “pointer”) I find this accidental pun pretty amusing :rofl:

I think the easiest way to figure out why you’re having this issue is to ask yourself “what is the lifetime of _z?”. In this case, ref_cell will immutably borrow vec in the vec.first() call. When you do _z = ref_cell, that then says the lifetime of _z is equal to the lifetime of that borrow. So your program is equivalent to:

use std::cell::RefCell;

fn main() {

    let mut vec: Vec<RefCell<i32>> = Vec::new();

    vec.push(RefCell::new(1));
    let _z = vec.first().unwrap();

    vec.push(RefCell::new(2)); 
    println!("{:?}", vec);
}

Which obviously isn’t going to compile. It should be noted that non-lexical lifetimes should solve these sorts of problems.


#3

Yep, this will be solved by non-lexical lifetimes. For now the borrow of vec lasts until the borrowed value _z goes out of scope at line 21. The borrow checker pessimistically assumes that you might want to read from _z at any point while it is in scope, so in that sense it correctly prevents the code you wrote. Annotated:

// Suppose this is initialized with a capacity of 1.
let mut vec: Vec<RefCell<i32>> = Vec::new();

// Now the capacity and length are both 1.
vec.push(RefCell::new(1));

let _z;
{
    let ref_cell: &RefCell<i32> = vec.first().unwrap();

    // _z points to the first element of the buffer allocated on the first line.
    _z = ref_cell;
}

// Does not fit in the original capacity, so reallocate a bigger buffer, copy
// elements, and destroy the small buffer.
vec.push(RefCell::new(2));

// Borrow checker assumes that you may still want to read from _z here, which
// is a problem because _z is now a dangling pointer. The non-lexical lifetimes
// borrow checker will recognize that you are not in fact reading from _z here
// so the code will work as written.

#4

Thanks Michael,

In this case, ref_cell will immutably borrow vec in the vec.first()

Okay i see, I was getting confused because I thought ref_cell would immutably borrow the first item in vec rather than immutably borrowing vec itself. I guess because the first item exists in vec and it is immutably borrowed than this causes vec to also be immutably borrowed.

Thanks again for the ‘pointers’. Slowly getting there with my understanding of the borrow checker :slight_smile:


#5

Thank you for the step by step run through dtolnay. This helps paint a clearer picture of whats going on for me. I can’t wait for NLL but at the same time its great to be able to think about why stuff like this is going wrong :slight_smile:


#6

I’m kinda half-half about NLL. On one hand it makes certain things a lot easier to do (i.e. the Entry API), but it also means other things are harder to understand. In this case you were having problems because of a misunderstanding about what lifetimes are tied to which variable.

With NLL you never encounter these issues early on, so things may still Just Work even when your mental model of lifetimes isn’t necessarily correct. That’s not great because you’ll have even more difficult lifetime issues later on (which may be fundamentally incorrect/impossible to solve).


#7

I take your point. Ill try to get as battle tested as I can before NLL hits and i become lazy :wink:


#8

I’d argue the current rules actually make learning lifetimes harder; just when you think you understand things you hit a lexical borrow error and start questioning your understanding (until you learn about lexical vs NLL). NLL will make you learn lifetimes when it actually makes a difference and needs to be understood, whereas the current implementation makes you also learn about a limitation in the current impl.