Returning a mutable reference extends its lifetime?

Hello, I’m once again confused by the borrow checker.
Here’s a toy example that fails to compile:

struct Foo<T>(T);

impl <T> Foo<T> {
    fn error(&mut self) -> &T {
        if let Some(v) = self.get() {
            v
        } else {
            &self.0
        }
    }
    
    fn works(&mut self) -> &T {
        &self.0
    }
    
    fn also_works(&mut self) -> &T {
        self.get().unwrap()
    }
    
    fn get(&mut self) -> Option<&mut T> {
        // Pretend this function does something reasonable 
        // and can return either Some or None
        panic!();
    }
}

Both works() and also_works() compile, but error() doesn’t - why?
The error message implies that the mutable borrow initiated by get() extends for the whole length of error(), even though its contents are only used in the first branch.

I’ve attempted rewriting the function in the following way that should preserve its semantics, while making the scopes of borrows clearer:

fn error(&mut self) -> &T {
    {
        let opt = self.get();
    
        if let Some(v) = opt {
            return v;
        } 
    }
    
    let r = &self.0;
    return r;
}

This looks especially strange to me: opt is out of scope by the time let r = &self.0 is reached, yet the compiler insists that the mutable borrow of self initiated by get() is still active at that point.

Does returning a reference cause its lifetime to extend? Why? I’d expect the opposite to happen - if the body of the function past the return is still executing, then the return didn’t occur, so the reference hasn’t been shared with the outside world and should be dropped by now.

I feel like either I’m missing something obvious, or it’s a borrow checker limitation and I just need to work around it.

Hey, I originally asked the question in Discord :slight_smile:. Here’s some more things I tried that didn’t work:

   fn error(&mut self) -> &T {
       match self.get() {
           Some(v) => return v,
           _ => &self.0
       }
   }
   fn error(&mut self) -> &T {
       match self.get() {
               Some(v) => return v,
               _ => {}
        }
       &self.0
   }
   fn error(&mut self) -> &T {
       // T was Option<_> in this case
       let saved = std::mem::replace(&mut self.0, None);
       if let Some(Locatable { location, .. }) = self.peek_token() {
               return location;
       }
       saved.as_ref().unwrap()
   }

Hi, this is a known limitation of the current borrow checker, there are a few issues reporting the problem like this one.
Polonius the next generation of the borrow checker will fix it. You can try it yourself by using cargo +nightly rustc -- -Z polonius.
There might be a workaround specific to your case like this topic but I’d need more information.

3 Likes

Fixed it! See https://github.com/rust-lang/rust/issues/21906#issuecomment-396003512 and https://play.rust-lang.org/?gist=98db57d5e84b456f107f7b3522722ba5&version=stable&mode=debug. If you ignore the result of get() and only check whether it’s Some or None, it compiles fine. Thank you for the help!

struct Foo<T>(T);

impl <T> Foo<T> {
    fn error(&mut self) -> &T {
        if let Some(_) = self.get() {
            self.get().unwrap()
        } else {
            &self.0
        }
    }
    
    fn works(&mut self) -> &T {
        &self.0
    }
    
    fn also_works(&mut self) -> &T {
        self.get().unwrap()
    }
    
    fn get(&mut self) -> Option<&mut T> {
        // Pretend this function does something reasonable 
        // and can return either Some or None
        panic!();
    }
}
1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.