Why does the borrow checker disallow a second mutable borrow even if the first one is already out of scope?
Asked Answered
M

2

4

Background

I know that the borrow checker disallows more than one mutable borrows. For example, the code below is invalid:

fn main() {
    let mut x = 42;
    let a = &mut x;
    let b = &mut x;
    println!("{} {}", a, b);
}

However, if the first borrow is dropped due to out of scope, the second borrow is valid:

fn main() {
    let mut x = 1;
    {
        let a = &mut x;
        println!("{}", a);
    }
    let b = &mut x;
    println!("{}", b);
}

Because of non-lexical lifetimes (NLL), the first borrow doesn't even have to be out of scope — the borrow checker only requires it not to be used anymore. So, the code below is valid in Rust 2018:

fn main() {
    let mut x = 1;

    let a = &mut x;
    println!("{}", a);

    let b = &mut x;
    println!("{}", b);
}

The Problem

But I don't understand why the code below is invalid:

use std::str::Chars;

fn main() {
    let s = "ab".to_owned();
    let mut char_iter = s.chars();

    let mut i = next(&mut char_iter);
    dbg!(i.next());

    let mut j = next(&mut char_iter);
    dbg!(j.next());
}

fn next<'a>(char_iter: &'a mut Chars<'a>) -> impl Iterator<Item = char> + 'a {
    char_iter.take_while(|&ch| ch != ' ')
}

Compile error message:

error[E0499]: cannot borrow `char_iter` as mutable more than once at a time
  --> src/main.rs:10:22
   |
7  |     let mut i = next(&mut char_iter);
   |                      -------------- first mutable borrow occurs here
...
10 |     let mut j = next(&mut char_iter);
   |                      ^^^^^^^^^^^^^^ second mutable borrow occurs here
11 |     dbg!(j.next());
12 | }
   | - first borrow might be used here, when `i` is dropped and runs the destructor for type `impl std::iter::Iterator`

From the error message I thought that maybe NLL doesn't support this case yet. So, I dropped i early:

use std::str::Chars;

fn main() {
    let s = "ab".to_owned();
    let mut char_iter = s.chars();

    {
        let mut i = next(&mut char_iter);
        dbg!(i.next());
    }

    let mut j = next(&mut char_iter);
    dbg!(j.next());
}

fn next<'a>(char_iter: &'a mut Chars<'a>) -> impl Iterator<Item = char> + 'a {
    char_iter.take_while(|&ch| ch != ' ')
}

(Rust Playground)

But I got a more confusing error message:

error[E0499]: cannot borrow `char_iter` as mutable more than once at a time
  --> src/main.rs:12:22
   |
8  |         let mut i = next(&mut char_iter);
   |                          -------------- first mutable borrow occurs here
...
12 |     let mut j = next(&mut char_iter);
   |                      ^^^^^^^^^^^^^^
   |                      |
   |                      second mutable borrow occurs here
   |                      first borrow later used here

Why does it say first borrow later used here even if i is already dropped and out of scope before?


An Alternative Approach

The code is compiled if I change the signature of next function into this:

fn next(char_iter: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
    char_iter.take_while(|&ch| ch != ' ')
}

But still, I want to understand why the original next function doesn't work.

Marlin answered 9/7, 2020 at 14:35 Comment(0)
V
4

Let's decipher this type deduction magic here. impl Iterator is actually a concrete type: Chars that wrapped with TakeWhile, so you may rewrite your method like this (btw, an interesting task is to determine the &char's lifetime):

fn next<'a>(
    char_iter: &'a mut Chars<'a>,
) -> TakeWhile<&'a mut Chars<'a>, impl FnMut(&char) -> bool> {
    char_iter.take_while(|&ch| ch != ' ')
}

Now you may see that the output type lives as long as the input and vice versa. That lifetime, in fact, derived from the &str that is originally used. Therefore, you may conclude that the resulted type lives as long as the used string (i.e. to the end of the main) and even explicit drop(i) won't help you, because compiler knows that the Chars is borrowed till the end. For working nll you must (unfortunately?) help the compiler:

fn next<'a, 'b: 'a>(
    char_iter: &'a mut Chars<'b>,
) -> TakeWhile<&'a mut Chars<'b>, impl FnMut(&char) -> bool> {
    char_iter.take_while(|&ch| ch != ' ')
}
Volatilize answered 9/7, 2020 at 16:40 Comment(2)
Nice to know that impl trait can be used in type parameters. I previously thought it can only be used in argument or return types as per the official doc.Marlin
I didn't know it either, I thought to give that example as a rough equivalent but it compiles to my surprise.Volatilize
M
2

The problem is that you're explicitly telling the borrow checker that i lives as long as char_iter in the following block by declaring both to have the same lifetime 'a.

fn next<'a>(char_iter: &'a mut Chars<'a>) -> impl Iterator<Item = char> + 'a {
    char_iter.take_while(|&ch| ch != ' ')
}

This means that the compiler believes that &mut char_iter is still in use as long as char_iter is still in scope. That is, until the end of main().

Mayst answered 9/7, 2020 at 16:44 Comment(1)
I think the real question is “Why does reducing the scope of i not work?"?Monachism

© 2022 - 2024 — McMap. All rights reserved.