Understanding cannot borrow as mutable error

According to the documentation

At any given time, you can have either one mutable reference or any number of immutable references.

So why is this flagged for error


fn first_word(s: &String) -> &str {
    "Hello"
}
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {}", word);
}

with the message error[E0502]: cannot borrow ``s`` as mutable because it is also borrowed as immutable

while this

fn first_word(s: &String) -> String {
    String::from("Hello")
}
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {}", word);
}

compiles without an error. In both the cases in the main() we do an immutable borrow when invoking first_word(). and then a mutable borrow while doing s.clear()

Why doesn’t the second version throw the cannot borrow as mutable error?

Thanks
-D

Hi, I’m not an expert but I think that’s what is happening:
The first code doesn’t work because by default when the compiler sees a reference in and a reference out it will do this but what you really want is that.
And the second code simply doesn’t have this issue.

4 Likes

In the second version, you really don’t use the borrow after the finish of the function call (because return value don’t reference it). Thanks to non-lexical lifetimes, the borrow is essentially immediately dropped and doesn’t interfere with further mutable borrowing.

In the first case, at the same time, return value is (according to the function signature) borrowing from the input. While it is alive, it holds the lock and doesn’t allow for mutable borrow to appear.

As @leudz said, this had to do with lifetime elision. If you don’t mark lifetimes, thrn Rust will try to infer the lifetimes from the signature alone, and sometimes doesn’t do what you want. Importantly, Rust doesn’t look at the function body to do lifetime analyses.

1 Like

I’m fairly sure this is lifetime elision at work, making the return type of first_word have the same lifetime at its input parameter.

fn first_word(s: &str) -> &str;               // elided
fn first_word<'a>(s: &'a str) -> &'a str;     // expanded

See https://doc.rust-lang.org/nomicon/lifetime-elision.html

1 Like

NLL doesn’t comes into play, you can compile just fine with edition 2015.

Thanks, seems that I misunderstood something. Does it mean that unused temporary borrows are dropped at the end of statement?

Thanks @leudz

Is there a way to see the lifetime elisions expanded somewhere as you showed in the playground or is it from experience? Where can I read more about

by default when the compiler sees a reference in and a reference out it will do

oops. Looks like @anderejd already gave me the answer.

Yes

It depends, if you assign it, used or not, it exists until the end of scope.

    fn main() {
        let mut s = String::from("hello world");
        let letter = s.get(0..1).unwrap();
        // this is dropped on the spot
        s.get(0..1).unwrap();
        s.clear();
        // letter dropped here NLL or not
    }

You are right, “dropped” is not the best word.
I would say that temporary borrows end at the end of a statement;
So, although letter is freed at the end of a scope (after s.clear() for instance), thanks to NLL, since there is no drop glue, the borrow can end before s.clear(), thus compiling, whereas without NLL the borrow ends when the reference is dropped, after s.clear(), thus not compiling.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.