Why does return extend borrowing to the entire function?

Im a bit surprised that the following function does not compile:

use std::str::FromStr;

fn borrow_test(string: &mut String) -> &str {
    {
        let str = string.as_mut_str();
        if str.starts_with('H') {
            return str;
        }
    }
    string.push('!');
    return string.as_str();
}

fn main() {
    let mut string = String::from_str("Hello").unwrap();
    println!("{}", borrow_test(&mut string));
}
   Compiling borrowtest v0.1.0
src/main.rs:10:5: 10:11 error: cannot borrow `*string` as mutable more than once at a time [E0499]
src/main.rs:10     string.push('!');
                   ^~~~~~
src/main.rs:10:5: 10:11 help: run `rustc --explain E0499` to see a detailed explanation
src/main.rs:5:19: 5:25 note: previous borrow of `*string` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `*string` until the borrow ends
src/main.rs:5         let str = string.as_mut_str();
                                ^~~~~~
src/main.rs:12:2: 12:2 note: previous borrow ends here
src/main.rs: 3 fn borrow_test(string: &mut String) -> &str {
               ...
src/main.rs:12 }
               ^
src/main.rs:11:12: 11:18 error: cannot borrow `*string` as immutable because it is also borrowed as mutable [E0502]
src/main.rs:11     return string.as_str();
                          ^~~~~~
src/main.rs:5:19: 5:25 note: previous borrow of `*string` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `*string` until the borrow ends
src/main.rs:5         let str = string.as_mut_str();
                                ^~~~~~
src/main.rs:12:2: 12:2 note: previous borrow ends here
src/main.rs: 3 fn borrow_test(string: &mut String) -> &str {
               ...
src/main.rs:12 }
               ^
error: aborting due to 2 previous errors
Could not compile `borrowtest`.

However if I remove the return str statement, it compiles.
Why does returning the str extend the borrowing scope to the entire function?

Note: This is an artificial example to show the problem. I know, I could just use as_str() but in my real code I can't.

1 Like

I don't really understand the details, but it looks like you need non-lexical borrows to be implemented, according to a very similar github issue and its related RFC.

1 Like

Yes, that seems to be the same issue.

But is there a way around? I thought I can limit the lexical scope where string is borrowed by adding a new scope {...} but that doesn't seem to work because returning will always escape that scope...

From what I can gather, no, there is no general resolution for the early return extending the borrow; not until non-lexical borrows are implemented, which itself is waiting on MIR. The only option remaining is to restructure the code so it's no longer an issue.

You can work around it by not making a variable. Here's a simplified version:

fn borrow_test(string: &mut String) -> &str {
    if string.starts_with('H') {
        return string;
    }
    string.push('!');
    return string;
}

fn main() {
    let mut string = String::from("Hello");
    println!("{}", borrow_test(&mut string));
}
2 Likes

I don't understand why this works and mine doesn't, it's still returning a borrowed str.

Unfortunately, my actual code isn't about strings and I cannot work around like that. I found a different "solution" but I don't actually like it.

I'm not 100% sure I can explain it either, but the variable may have managed to extend the borrow past the return, or something.

It works because there is not an additional mutable borrow like in your original code. String::starts_with takes an immutable borrow which ends with the closing of the if. You only mutate the parameter after that, and since the immutable borrow just ended you only have a single mutable borrow, and everything is fine. The mutable borrow itself ends with the function, so it's safe to return it as immutable.

1 Like