Immutable Borrow in None Option

Viewed 95

Example Code:

struct Foo;

impl Foo {
    fn foo(&mut self) -> Result<&i32, String> {
        match self.bar() {
            Some(d) => Ok(d),
            None => {
                if self.check() {
                    Err(String::from("Error type 1"))
                } else {
                    Err(String::from("Error type 2"))
                }
            }
        }
    }
    
    fn bar(&mut self) -> Option<&i32> {
        todo!()
    }
    
    fn check(&self) -> bool {
        todo!()
    }
}

Rust playground Code


Problem

  • In the Foo::foo function:
    • self.bar() requires mutable borrow.
    • self.check() requires immutable borrow.

The above code fails to compile. The lifetime checker complains about the two borrow (mutable and immutable).

error[E0502]: cannot borrow `*self` as immutable because it is also borrowed as mutable
 --> src/lib.rs:8:20
  |
4 |     fn foo(&mut self) -> Result<&i32, String> {
  |            - let's call the lifetime of this reference `'1`
5 |         match self.bar() {
  |               ---------- mutable borrow occurs here
6 |             Some(d) => Ok(d),
  |                        ----- returning this value requires that `*self` is borrowed for `'1`
7 |             None => {
8 |                 if self.check() {
  |                    ^^^^^^^^^^^^ immutable borrow occurs here

Questions

Question 1

Why the compiler prevent me to compile this code?

Clearly I am missing something here, but...

The immutable borrow (i.e. self.check()) only happens in the None branch. The previous mutable borrow (i.e. self.bar()) should not bring any impact there, should it?

The same code might be written:

if let Some(d) = self.bar() {
   return Ok(d);
}

// What lifetime of `self.bar` is needed at this point?

if self.check() {
  // ...
} else {
  // ...
}

Question 2

How can I solve this problem?

Please note:

  • I do not want to move the check (i.e. self.check()) before the match. Merely because of performances: self.check might be expensive and in the "cold" path. If self.bar() returns Some I want to return as soon as possible from the function.
  • I do not want to to introduce runtime overhead. (E.g., dynamic check for borrow -> RefCell).
  • Of course, this is a dummy example. Just for the sake of demonstration. So be aware about it. For example:
    • &'a i32 represent an example returned "data" that requires lifetime. In my real code you can imagine I have a complex object which holds some references (and thus require lifetime). E.g., struct ComplexObject<'a>.
1 Answers

Answer 1

The problem is that in Some(d) => Ok(d) you're still holding a reference &i32 with the same lifetime as &mut self of the self.bar() call. Lifetime elisions hide this, but the code would desugar into something like this:

fn bar<'a>(&'a mut self) -> Option<&'a i32> {
    todo!()
}

The self.bar() result has lifetime 'a, so does d in Some(d). Since you're returning d from the function, self.bar() needs to stay mutably borrowed until the end of the function. This makes self.check() impossible to be borrowed.

To better visualize the issue:

use std::rc::Rc;

struct Foo;

impl Foo {
    fn foo<'b>(&'b mut self) -> Result<&'b i32, String> {
        match self.bar() {        // --+- "&'a mut self" borrowed
            Some(d) => Ok(d),     // --+- "'a" becomes "'b"
            None => {             //   |
                if self.check() { // --+- "&'c self" borrow failed attempt
                    Err(String::from("Error type 1"))
                } else {          //   |
                    Err(String::from("Error type 2"))
                }                 //   |
            }                     //   |
        }                         //   |
    }                             // --+- "&'a mut self" valid until here
    
    fn bar<'a>(&'a mut self) -> Option<&'a i32> {
        todo!()
    }
    
    fn check<'c>(&'c self) -> bool {
        todo!()
    }
}

The return value of self.foo is &'b i32, which needs the same lifetime as the return value of bar, so 'a needs to live at least as long as 'b, thus &'a mut self borrowed in self.bar stays borrowed until the end of self.foo.

Answer 2

There's three options I'd consider:

  1. Change the return value of bar to be Option<i32> if moving is an option:
struct Foo;

impl Foo {
    fn foo(&mut self) -> Result<i32, String> {
        match self.bar() {
            Some(d) => Ok(d),
            None => {
                if self.check() {
                    Err(String::from("Error type 1"))
                } else {
                    Err(String::from("Error type 2"))
                }
            }
        }
    }
    
    fn bar(&mut self) -> Option<i32> {
        todo!()
    }
    
    fn check(&self) -> bool {
        todo!()
    }
}
  1. Split self.bar() into two separate functions, one that does the mutable operation and moves the ownership from the function to the callee, and one that returns Option<&i32> but borrows immutably:
struct Foo;

impl Foo {
    fn foo(&mut self) -> Result<&i32, String> {
        let bar = self.bar_mut();
        match self.bar(bar) {
            Some(d) => Ok(d),
            None => {
                if self.check() {
                    Err(String::from("Error type 1"))
                } else {
                    Err(String::from("Error type 2"))
                }
            }
        }
    }
    
    fn bar_mut(&mut self) -> Option<i32> {
        todo!()
    }
    
    fn bar(&self, bar: Option<i32>) -> Option<&i32> {
        todo!()
    }
    
    fn check(&self) -> bool {
        todo!()
    }
}
  1. Use Rc for multiple ownership if you cannot move the object at all:
use std::rc::Rc;

struct Foo;

impl Foo {
    fn foo(&mut self) -> Result<Rc<i32>, String> {
        match self.bar() {
            Some(d) => Ok(d),
            None => {
                if self.check() {
                    Err(String::from("Error type 1"))
                } else {
                    Err(String::from("Error type 2"))
                }
            }
        }
    }
    
    fn bar(&mut self) -> Option<Rc<i32>> {
        // Rc::clone the value here
        todo!()
    }
    
    fn check(&self) -> bool {
        todo!()
    }
}
Related