Lifetimes and borrowing question

Hello,

I don't understand why this code does not compile when I comment the line (L). What is the difference between both cases (when commented and uncommented).

#![allow(unused)]
fn main() {
    let mut b = Box::new(99);
    let mut r = &b;
    for i in 0..10 {
    	println!("{}", r);
    	b = Box::new(i);
    	r = &b;     // (L)  <-- when I comment this line, it does not compile, why?
    }
    println!("{}", r);
}

Your r is a shared reference to b: from the pointer where it is assigned, up until the point of last use, there can only be other shared accesses to b, there can't be exclusive accesses to b.

At line b = ..., you assign to b, which requires unique access to b. This contradicts the outstanding shared borrow r of b, unless that shared borrow is not used anymore.

Another way of seeing this is that when performing the assignment, since it requires unique / exclusive access to b, any outstanding reference gets "invalidated" / deactivated. If you try to use it (as in the println!("{}", r); statement), then the borrow checker will trigger a compilation error.

When you do r = &b;, however, you are not using a stale / invalidated pointer (you are not reading its value) but rather, you are (re-)updating the borrow as a new one (new point of assignment), which then reasserts, from that point onwards, lack of exclusive access to b ... until the point of last use. And so on and so forth.

let mut b = Box::new(99);
let mut r = &b; // ----------+ shared borrow starts here and spans until...
let i = 0; { //              |
	println!("{}", r); // <--+ ...the point of last use

	b = Box::new(i); // ⚠️ exclusive access must not collide with shared one ✅

	r = &b;     // ----------+ shared borrow resumes here and spans until...
} //                         |
let i = 1; { //              |
	println!("{}", r); // <--+ ...the point of last use

	b = Box::new(i); // ⚠️ exclusive access must not collide with shared one ✅

	r = &b;     // ----------+ shared borrow resumes here and spans until...
} //                         |
...
let i = 9; { //              |
	println!("{}", r); // <--+ ...the point of last use

	b = Box::new(i); // ⚠️ exclusive access must not collide with shared one ✅

	r = &b;     // ----------+ shared borrow resumes here and spans until...
} //                         |
println!("{}", r); // <------+ ...the point of last use

So this is a fine snippet.

But if you commented out that re-assignment line, then you'd be having a single very largely spanned borrow, which would necessarily overlap over that incompatible assignment:

let mut b = Box::new(99);
let mut r = &b; // ----------+ shared borrow starts here and spans until...
let i = 0; { //              |
	println!("{}", r); //    |
	b = Box::new(i); // excl💥sive access must not collide with shared one
} //                         |
let i = 1; { //              |
	println!("{}", r); //    |
	b = Box::new(i); // excl💥sive access must not collide with shared one
} //                         |
... //                       |
let i = 9; { //              |
	println!("{}", r); //    |
	b = Box::new(i); // excl💥sive access must not collide with shared one
} //                         |
println!("{}", r); // <------+ ...the point of last use

Aside

It is interesting to see that all these assignments are being done to the same variable r, which ought to have the lifetime embedded inside its type. So this means that the "lifetime parameter appearing in the type of r", if such a thing existed, is representing a non-continuous region of code! It's quite fascinating, while also something that cannot be expressed, at least not before polonius, when using actual generic lifetime parameters at function boundaries (an attempt)

5 Likes

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.