struct Test {
inner: Vec<u8>,
}
impl Test {
fn fails_but_why(&mut self) -> &mut u8 {
// a fairly expensive/recursive call here
if let Some(last) = self.inner.last_mut() {
return last;
}
// only the plain reference is required at this point
let as_ref = self.inner.as_slice();
todo!("do something with `&[u8]`")
}
// is NOT something I'm willing to settle on
fn works(&mut self) -> &mut u8 {
let is_some = self.inner.last_mut().is_some();
match is_some {
true => self.inner.last_mut().unwrap(),
false => {
let as_ref = self.inner.as_slice();
todo!("do something with `&[u8]`")
}
}
}
}
I'm assuming it is yet another edge case, not handled properly by the current implementation of the compiler? Is there any well-known workaround, that would not require turning the first mutable borrow into a boolean true/false check, simply to "detach" the lifetime from the first call, before branching into either the former if let or the following as_ref?
Yeah it looks like the limitation of the current borrow checker. I guess the checker thinks the &mut lifetime is extended until the entire method execution end because a value that depends on said &mut is returned to the outside. Then the checker see &, so it gives compile time errors can't use immutable reference because mutable reference is still active. It looks like only happen with &mut
While logical analysis, the immutable reference ends directly because there is return keyword, the execution is returned to the caller, so the immutable reference does not get a chance to run
The workaround without additional runtime branching will need to revert the order, & at the top, &mut at the bottom. And making sure the & ends before entering &mut
This is what I can think of
struct Test {
inner: Vec<u8>,
}
impl Test {
fn fails_but_why(&mut self) -> &mut u8 {
// put the immutable reference branch at the top
if let None = self.inner.last_mut() {
let as_ref = self.inner.as_slice();
todo!("do something with `&[u8]`")
}
// put the mutable reference branch at the bottom. the immutable reference lifetime ends in the end of the if let None
// safe to call unwrap because it is guaranted this is the Some(val) branch
self.inner.last_mut().unwrap()
}
// is NOT something I'm willing to settle on
fn works(&mut self) -> &mut u8 {
let is_some = self.inner.last_mut().is_some();
match is_some {
true => self.inner.last_mut().unwrap(),
false => {
let as_ref = self.inner.as_slice();
todo!("do something with `&[u8]`")
}
}
}
}
fn main() {
}
This is less “yet another” edge case and more the one big problem case: conditional returns of exclusive borrows. When the borrow checker sees an exclusive (mutable) borrow which may be returned, it treats it as if its lifetime extends to the end of the function, even if it was dropped instead of returned. In this particular case, the problematic borrow is the &mut self.inner borrow implicitly created by the call to last_mut().
The solution, in general, is to avoid creating the exclusive borrow until such time as you are certain you are going to return it. In this case, that can be done with pattern matching (which does not borrow until the match succeeds); the following code compiles:
impl Test {
fn works(&mut self) -> &mut u8 {
match self.inner.as_mut_slice() {
[.., last] => last,
other => todo!("do something with `&[u8]`"),
}
}
}
However, since the slice in the second case is always going to be empty, your real situation is presumably more complex in some way, so further changes in strategy may be needed; if you show more details I can try to find a rewrite that works.
The fallback, if you can't find a way to do it within the language, is to use polonius_the_crab, a library that uses unsafe to enable this particular code structure.
It requires some control-flow analysis, to say that borrows have different lengths depending on which branch is taken. It’s still feasible enough to implement that it has in fact been implemented in the Polonius borrow checker, it’s just not on stable Rust yet.
Part of the challenge is implementing such an analysis efficiently. Something[1] that could handle this particular case was described in the NLL RFC (and AFAIK was even implemented), but some pieces needed for this use case ended up being scrapped as they hurt compilation time too much.
Polonius takes a different approach which required a larger rewrite.[2] If you want to see the PRs that have gone into the current implementation, lots of them are linked from here.[3] Skimming comments, I think there will still be a performance hit,[4] just not as severe, and there's at least one unsoundness being actively worked on.