Why does return extend borrowing to the entire function?


#1

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.


#2

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.


#3

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…


#4

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.


#5

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));
}

#6

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.


#7

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.


#9

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.