16

Why does the call self.f2() in the following code trip the borrow checker? Isn't the else block in a different scope? This is quite a conundrum!

use std::str::Chars;

struct A;

impl A {
    fn f2(&mut self) {}

    fn f1(&mut self) -> Option<Chars> {
        None
    }

    fn f3(&mut self) {
        if let Some(x) = self.f1() {

        } else {
            self.f2()
        }
    }
}

fn main() {
    let mut a = A;
}

Playground

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:16:13
   |
13 |         if let Some(x) = self.f1() {
   |                          ---- first mutable borrow occurs here
...
16 |             self.f2()
   |             ^^^^ second mutable borrow occurs here
17 |         }
   |         - first borrow ends here

Doesn't the scope of the borrow for self begin and end with the self.f1() call? Once the call from f1() has returned f1() is not using self anymore hence the borrow checker should not have any problem with the second borrow. Note the following code fails too...

// ...
if let Some(x) = self.f1() {
    self.f2()
}
// ...

Playground

I think the second borrow should be fine here since f1 and f3 are not using self at the same time as f2.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Sandeep Datta
  • 28,607
  • 15
  • 70
  • 90

5 Answers5

11

I put together an example to show off the scoping rules here:

struct Foo {
    a: i32,
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("Foo: {}", self.a);
    }
}

fn generate_temporary(a: i32) -> Option<Foo> {
    if a != 0 { Some(Foo { a: a }) } else { None }
}

fn main() {
    {
        println!("-- 0");
        if let Some(foo) = generate_temporary(0) {
            println!("Some Foo {}", foo.a);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(foo) = generate_temporary(1) {
            println!("Some Foo {}", foo.a);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(Foo { a: 1 }) = generate_temporary(1) {
            println!("Some Foo {}", 1);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(Foo { a: 2 }) = generate_temporary(1) {
            println!("Some Foo {}", 1);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
}

This prints:

-- 0
None
-- 1
-- 0
Some Foo 1
Foo: 1
-- 1
-- 0
Some Foo 1
Foo: 1
-- 1
-- 0
None
Foo: 1
-- 1

In short, it seems that the expression in the if clause lives through both the if block and the else block.

On the one hand it is not surprising since it is indeed required to live longer than the if block, but on the other hand it does indeed prevent useful patterns.

If you prefer a visual explanation:

if let pattern = foo() {
    if-block
} else {
    else-block
}

desugars into:

{
    let x = foo();
    match x {
    pattern => { if-block }
    _ => { else-block }
    }
}

while you would prefer that it desugars into:

bool bypass = true;
{
    let x = foo();
    match x {
    pattern => { if-block }
    _ => { bypass = false; }
    }
}
if not bypass {
    else-block
}

You are not the first one being tripped by this, so this may be addressed at some point, despite changing the meaning of some code (guards, in particular).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
6

It's annoying, but you can work around this by introducing an inner scope and changing the control flow a bit:

fn f3(&mut self) {
    {
        if let Some(x) = self.f1() {
            // ...
            return;
        }
    }
    self.f2()
}

As pointed out in the comments, this works without the extra braces. This is because an if or if...let expression has an implicit scope, and the borrow lasts for this scope:

fn f3(&mut self) {
    if let Some(x) = self.f1() {
        // ...
        return;
    }

    self.f2()
}

Here's a log of an IRC chat between Sandeep Datta and mbrubeck:

mbrubeck: std:tr::Chars contains a borrowed reference to the string that created it. The full type name is Chars<'a>. So f1(&mut self) -> Option<Chars> without elision is f1(&'a mut self) -> Option<Chars<'a>> which means that self remains borrowed as long as the return value from f1 is in scope.

Sandeep Datta: Can I use 'b for self and 'a for Chars to avoid this problem?

mbrubeck: Not if you are actually returning an iterator over something from self. Though if you can make a function from &self -> Chars (instead of &mut self -> Chars) that would fix the issue.

fghj
  • 8,898
  • 4
  • 28
  • 56
mbrubeck
  • 564
  • 3
  • 7
  • 1
    This works even if you remove the braces around the `if let` expression. – Sandeep Datta May 14 '15 at 17:59
  • Answer accepted because you also gave a work around in addition to an explanation. – Sandeep Datta May 15 '15 at 16:09
  • However I think the follow up question has not been answered fully. "self remains borrowed as long as the return value from f1 is in scope." sounds like an artifact of the way the borrow checker works now. There are several different lifetimes conflated with each other here the `self.f1` borrow does not have the same lifetime as the `self.f3` borrow still they are represented by the same lifetime `'a`. – Sandeep Datta May 15 '15 at 16:17
4

As of Rust 2018, available in Rust 1.31, the original code will work as-is. This is because Rust 2018 enables non-lexical lifetimes.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
3

A mutable reference is a very strong guarantee: that there's only one pointer to a particular memory location. Since you've already had one &mut borrow, you can't also have a second. That would introduce a data race in a multithreaded context, and iterator invalidation and other similar issues in a single-threaded context.

Right now, borrows are based on lexical scope, and so the first borrow lasts until the end of the function, period. Eventually, we hope to relax this restriction, but it will take some work.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Steve Klabnik
  • 14,521
  • 4
  • 58
  • 99
3

Here is how you can get rid of the spurious errors. I am new to Rust so there may be serious errors in the following explanation.

use std::str::Chars;

struct A<'a> {
    chars: Chars<'a>,
}

The 'a here is a lifetime parameter (just like template parameters in C++). Types can be parameterised by lifetimes in Rust.

The Chars type also takes a lifetime parameter. What this implies is that the Chars type probably has a member element which needs a lifetime parameter. Lifetime parameters only make sense on references (since lifetime here actually means "lifetime of a borrow").

We know that Chars needs to keep a reference to the string from which it was created, 'a will probably be used to denote the source string's lifetime.

Here we simply supply 'a as the lifetime parameter to Chars telling the Rust compiler that the lifetime of Chars is the same as the lifetime of the struct A. IMO "lifetime 'a of type A" should be read as "lifetime 'a of the references contained in the struct A".

I think the struct implementation can be parameterised independently from the struct itself hence we need to repeat the parameters with the impl keyword. Here we bind the name 'a to the lifetime of the struct A.

impl<'a> A<'a> {

The name 'b is introduced in the context of the function f2. Here it is used to bind with the lifetime of the reference &mut self.

fn f2<'b>(&'b mut self) {}

The name 'b is introduced in the context of the function f1.This 'b does not have a direct relationship with the 'b introduced by f2 above.

Here it is used to bind with the lifetime of the reference &mut self. Needless to say this reference also does not have any relationship with the &mut self in the previous function, this is a new independent borrow of self.

Had we not used explicit lifetime annotation here Rust would have used its lifetime elision rules to arrive at the following function signature...

//fn f1<'a>(&'a mut self) -> Option<Chars<'a>>

As you can see this binds the lifetime of the reference &mut self parameter to the lifetime of the Chars object being returned from this function (this Chars object need not be the same as self.chars) this is absurd since the returned Chars will outlive the &mut self reference. Hence we need to separate the two lifetimes as follows...

fn f1<'b>(&'b mut self) -> Option<Chars<'a>> {
    self.chars.next();

Remember &mut self is a borrow of self and anything referred to by &mut self is also a borrow. Hence we cannot return Some(self.chars) here. self.chars is not ours to give (Error: cannot move out of borrowed content.).

We need to create a clone of self.chars so that it can be given out.

Some(self.chars.clone())

Note here the returned Chars has the same lifetime as the struct A.

And now here is f3 unchanged and without compilation errors!

fn f3<'b>(&'b mut self)  {
    if let Some(x) = self.f1() { //This is ok now

    } else {
        self.f2() //This is also ok now
    }
}

The main function just for completeness...

fn main() {
    let mut a = A { chars:"abc".chars() };

    a.f3();

    for c in a.chars {
        print!("{}", c);
    }
}

I have updated the code the make the lifetime relationships clearer.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Sandeep Datta
  • 28,607
  • 15
  • 70
  • 90