- if the intuition about the soundness of the code is correct
- if we can teach the borrow checker new tricks to accept this behavior (or if there is something in the works already that I couldn’t find in my searches)
Old example which is just a special case of what NLL already handles
The problem can be seen in the following piece of code:
struct Resource {
accessed: usize
}
impl Resource {
fn access(&mut self) -> &usize {
self.accessed += 1;
&self.accessed
}
}
fn main() {
let mut res = Resource { accessed: 0 };
// { // uncomment to appease the borrow checker
let accessed = res.access();
assert_eq!(*accessed, 1);
// } // uncomment to appease the borrow checker
assert_eq!(res.accessed, 1);
}
Method access
mutably borrows self
and leaks an immutable reference to one of the sub-fields. Now, my intuition would say that this particular method has no way to leak a mutable reference to self
, so after calling the method self
should be just borrowed immutably. However, the borrow checker is not convinced, since it requires the leaked reference to be dropped before ending the mutable borrow on self
.
Now, if we just inline the body of access
into main
, the borrow checker is happy to notice where the mutability of the borrow ends:
struct Resource {
accessed: usize
}
fn main() {
let mut res = Resource { accessed: 0 };
let accessed = {
res.accessed += 1;
&res.accessed
};
assert_eq!(*accessed, 1);
assert_eq!(res.accessed, 1);
}
Would it be possible (feasible & desirable) to trace the mutability of the borrow across function call boundaries so that the first example would be accepted by the borrow checker as well?
Later edit: The initial example did not correctly present the core problem, as the upcoming non-lexical lifetimes feature would make that code compile. Thanks to the perceptiveness of the first three commenters in this thread (@Ixrec, @kennytm and @atagunov), this error was identified. Moreover, @atagunov provided an accurate example of the problem in a comment below.
After further research, I found that very problem presented in the Rustonomicon (if you are thinking “you should feel bad for not finding this before posting”, rest assured that I do ).
struct Foo;
impl Foo {
fn mutate_and_share(&mut self) -> &Self { &*self }
fn share(&self) {}
}
fn main() {
let mut foo = Foo;
let loan = foo.mutate_and_share();
foo.share();
}
fails with:
error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
--> src/main.rs:11:5
|
10 | let _loan = foo.mutate_and_share();
| --- mutable borrow occurs here
11 | foo.share();
| ^^^ immutable borrow occurs here
12 | }
| - mutable borrow ends here
As the Rustonomicon explains, the problem is that the lifetime of the borrow &mut self
which takes place when mutate_and_share
is called, must last as long as _loan
in order to avoid _loan
becoming a dangling reference. And that is great! What is not great is that the borrow stays mutable throughout, and we really don’t need it to be mutable for the lifetime of _loan
because _loan
is not mutable.
So I would like to open the discussion regarding the possibility to trace the mutability separately and convert the mutable borrow into an immutable one as soon as it no longer needs to be mutable.