Experimenting with Rust's drop scopes caused me some headache recently. Consider the following example:
use std::cell::RefCell;
fn main() {
let ref_cell = RefCell::new(7);
// Code doesn't panic if the `&`s are removed:
let _x = &*ref_cell.borrow_mut();
let _y = &*ref_cell.borrow_mut();
}
The above code causes a panic:
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:7:25
If I write *ref_cell.borrow_mut()
instead of &*ref_cell.borrow_mut()
, then the code doesn't panic.
If I understand it right, then the panic in case of using &*…
happens because of what the Rust reference calls "temporary lifetime extension":
The temporary scopes for expressions in
let
statements are sometimes extended to the scope of the block containing thelet
statement.
However, the reference also warns:
Note: The exact rules for temporary lifetime extension are subject to change. This is describing the current behavior only.
Does that mean the original code above might be compile-time error in future? Or would this only happen during an edition change?
I found an old Issue #39283, whichs seems to talk about the lifetime extension as a "leftover from the old days of Rust". So I wonder what's up with this feature/behavior?
I'm a bit irritated that the reference explicitly states that the exact rules for temporary lifetime extension are subject to change; yet I don't get a warning with the default compiler settings. But perhaps I also misunderstand something.
The reason why I started to think on this issue was this thread: Reference Decisions. Directly borrowing from a smart-pointer, RefMut
, etc. seems to be a nearby thing to do. However, if that behavior is indeed subject to change, then that might be a thing to warn people about?
Of course, in case of the above example, it wouldn't matter if it resulted in a compile-time error in future, because the example causes a panic anyway.
But consider this:
use std::cell::RefCell;
fn increment(x: &mut i32) {
*x += 1;
}
fn main() {
let ref_cell = RefCell::new(7);
{
let r = &mut *ref_cell.borrow_mut();
increment(r);
}
println!("Value = {}", ref_cell.into_inner());
}
If I understand it right, this code only works because the lifetime of the temporary RefMut
value (as returned by RefCell::borrow_mut
) is extended to the end of the inner block (such that the mutable borrow still lives when increment(r);
is executed. Couldn't such code be invalid if the rules on temporary lifetime extension were changed?
One more thing below.
I also got confused (but meanwhile understand) why the following example works:
use std::cell::RefCell;
fn increment(x: &mut i32) {
*x += 1;
}
fn main() {
let ref_cell = RefCell::new(7);
increment(&mut *ref_cell.borrow_mut());
increment(&mut *ref_cell.borrow_mut());
println!("Value = {}", ref_cell.into_inner());
}
There is no let
statement involved with the borrow_mut
operation, so temporary lifetime extension should not happen here. My confusion was caused by the reference stating:
Each variable or temporary is associated to a drop scope. When control flow leaves a drop scope all variables associated to that scope are dropped in reverse order of declaration (for variables) or creation (for temporaries).
[…]
Given a function, or closure, there are drop scopes for:
- The entire function
- […]
- Each expression
- […]
I thought &mut *ref_cell.borrow_mut()
is an expression, so I wrongly concluded that after the expression has been evaluated, the temporary value ref_cell.borrow_mut()
will be dropped. But it doesn't. I didn't understand until I read the next subsection "Scopes of function parameters":
All function parameters are in the scope of the entire function body, so are dropped last when evaluating the function. […]
I wanted to give feedback that mentioning this important detail in the next subsection is perhaps a bit confusing. It's not really a huge problem (as I guess I'm supposed to read the whole chapter first before making conclusions), but I would not be surprised if other people get confused too, when they try to understand how dropping order really is. And drop order seems to be an issue that causes a lot of confusion.