BiagioF
BiagioF

Reputation: 9705

Immutable Borrow in None Option

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

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:

Upvotes: 2

Views: 174

Answers (1)

Idefixx
Idefixx

Reputation: 472

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!()
    }
}

Upvotes: 1

Related Questions