Borrowing from a mutable reference

Hello, I am a Rust newbie and have a question on how mutable references are borrowed. Following is a sample code to illustrate the question.

Case 1:

fn main() {
    let mut x = String::from("hello");
    let y = &mut x;
    let z = y;
    // append(y);
    println!("{}", y)
}

fn append(p0: &mut String) {
    p0.push_str(" world");
}

The above correctly fails saying that 'y' has been moved to 'z', and no longer valid.

Case 2:

However, if I comment out:
let z = y;
and uncomment:
// append(y);

'y' is still valid, after append() returns. Why was 'y' not moved when append() was invoked ?

Is it because the mutable reference 'y' was copied while calling append(), and the borrow checker could verify that only one mutable reference was active ?

Why was it moved in Case 1, but copied in Case 2 ?

Thank you.

As a convenience, y is automatically reborrowed when you pass it to a function, as if you had written:

append(&mut *y);

The reborrow creates a new mutable reference, containing the same address but with a shorter lifetime, to be passed to the function. It is exactly a copy in the compiled machine code, but it is not a copy to the borrow checker because the lifetime changed.

You cannot use y while the reborrow exists, but y itself hasn't been moved away. (These are the same rules which apply to borrowing an owned value.)

3 Likes

So, I guess one should see (a) passing a reference to a function, as tad different from (b) assiging a reference to another variable.

The above is also corroborated out by the fact that
(a) assignment of immutable references are 'copy' (Copy trait implemented on &T), while
(b) assignment of mutable references are 'move' (Copy trait not implemented on &mut T), for most types 'T'.

Thanks.

Function calls don't always behave differently from assignment in this way. It's also possible for passing a mutable reference to a function to be “definitely a move”, in the case where the function requires the reference to be of an existing lifetime:

fn push_mut<'a>(element: &'a mut String, vec: &mut Vec<&'a mut String>) {
    vec.push(element);
    // cannot use `element` again because it has been moved into the vector
}

Here, the Vec has the reference with lifetime 'a in its type, so element will never be usable until the lifetime 'a ends, and since 'a is a parameter to push_mut(), 'a definitely won't end before push_mut() returns.

It doesn't actually matter whether this is “a move” or “a reborrow with lifetime equal to the original”. It would probably make sense to say that if the lifetime isn't shortened then it isn't a reborrow, but that's a choice of words to describe the situation, not a choice of which programs are valid (as far as I know).

You can ask "Will the compiler tell me that I can't use the reference because it moved, or that I can't use the reference because it is still borrowed mutably?" but that doesn't tell you much; you just need to know they both are possible and they are arguably the same thing from different angles.

1 Like

Well articulated reply. Thank you so much.

It's a bit more nuanced than that. The reborrow works kinda-sorta like a coercion. Rust doesn't have a spec yet and the official documentation on reborrows is surprisingly lacking, so I can't give a citation on when exactly automatic reborrows happen or don't happen. But we can get some approximation by looking at where coercions happen.

The relevant part for passing a reference to a function is

Arguments for function calls

The value being coerced is the actual parameter, and it is coerced to the type of the formal parameter.

And this can matter when you have a generic formal parameter instead of a concrete type. Consider this example, where append now takes a generic parameter by value:

fn main() {
    let mut x = String::from("hello");
    let y = &mut x;
    append(y);
    println!("{}", y)
}

fn append<N: NeedlesslyGeneric>(p0: N) {
    p0.push_str(" world");
}

It errors because y is moved again -- because the formal parameter wasn't a &mut String,[1] the automatic reborrow does not take place. But you can do a "manual" reborrow:

-    append(y);
+    append(&mut *y);

And now it works.


Now that you've seen a manual reborrow solve some scenario, let's return to this code that errors because you moved y:

fn main() {
    let mut x = String::from("hello");
    let y = &mut x;
    let _z = y;
    println!("{}", y)
}

Does a manual reborrow work here too? Yes, it does.

And now let me draw your attention to another coercion site:

let statements where an explicit type is given.

Because of that, this version also compiles.

fn main() {
    let mut x = String::from("hello");
    let y = &mut x;
    let _z: &mut _ = y;
    println!("{}", y)
}

So sometimes passing a reference reborrows and sometimes it doesn't; sometimes assigning a reference to another variable reborrows and sometimes it doesn't. [2]

This can all feel quite overly complex when first learning about it, but in practice reborrows mostly happen when you need them to, and you don't really think about them outside of debugging some subset of borrow check errors. I.e. it mostly "just works", and I feel most newcomers don't even register "but &mut isn't copy and I passed it to a function, so why can I still use it" as you did.


  1. more precisely, because there wasn't an explicit &mut ↩︎

  2. Passing a reference will usually reborrow as it's relatively rare for &mut _ to be the trait implementor you need to pass by value / most &mut-accepting parameters are explicitly &mut. And assigning a &mut you hold to some other variable is a rare thing to need to do in contrast with just using the &mut you already hold. So perhaps the main difference between the two is how likely you are to run into a scenario when a reborrow didn't happen even though you wanted it to. ↩︎

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.