Why is this OK? (multiple mutable references to same data; reborrows; NLL)

It looks to the naked eye like this is creating 2 mutable references to the same slice. Why is this OK? (this contrived example is a reduction of a real world discussion!)

fn get_mut_slice(r: &mut [f32]) -> &mut [f32] {
    r
}

fn main() {
    let mut v = vec![0f32;10];
    let r1 : &mut [f32] = &mut v;
    let r2 : &mut [f32] = get_mut_slice(r1);
    r1[0] = r2[0];
}

I have a strong suspicion that NLL are figuring out some way that this is OK. Is there a way to see what the NLL transformation has done?

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/playground`

3 Likes

We can simplify example a bit by getting rid of vector, function and one of the two explicit types:

fn main() {
    let mut num = 0;
    let r1 = &mut num;
    let r2: &mut _ = r1;
    *r1 = *r2;
}

But it stops to compile if we remove the type on r2, since then compiler will complain that r1 is moved from (here it is probably implicitly reborrowing, i.e. r2 = &mut *r1):

fn main() {
    let mut num = 0;
    let r1 = &mut num;
    let r2 = r1;
    // use of moved value: `r1`
    *r1 = *r2;
}

And also doesn't compile if we swap the order of borrows in the last line:

fn main() {
    let mut num = 0;
    let r1 = &mut num;
    let r2: &mut _ = r1;
    // cannot use `*r1` because it was mutably borrowed
    *r2 = *r1;
}

So I guess the valid example is actually interpreted like this:

fn main() {
    let mut num = 0;
    let r1 = &mut num;
    // r1 is reborrowed into r2 and temporarily unusable
    let r2: &mut _ = &mut *r1;
    {
        let tmp = *r2;
        // r2 is not used anymore, reborrow is released, r1 is available
        *r1 = tmp;
    }
}

...which compiles due to the NLL.

6 Likes

Generally, it's okay to have multiple mutable references to the same thing when one of them was created from the other.

3 Likes

So is something like this actually OK? Does rust know that the result &mut "was created from" the &mut slice?

pub fn from_slice_mut(slice: &mut [f32]) -> & mut [f32]
{
    unsafe { std::slice::from_raw_parts_mut(slice.as_mut_ptr(), slice.len()) }
}

Edit: Thinking about this further, the way I pronounce the lifetimes in the signature is "slice outlives the result" but maybe I should think about it differently?

This is a great pageful! I definitely learned stuff here. Thanks! (IMO something like this should definitely be in the rust book. A search on "reborrow" doesn't even have a match on the rust book!

Yes, your from_slice_mut is perfectly fine.

The main rule for having several mutable references is that when you take mutable reference A, and use it to create mutable reference B, then you're not allowed to use A between the creation and last use[1] of B.

This means that at any one time, you actually only have one mutable reference that's usable. All of the others are disabled by the above rule.


  1. Precisely what last use means is a bit complicated. ↩︎

2 Likes

Note that this is using lifetime elision. Regardless of what you put in the body of the function, that means

pub fn from_slice_mut<'a>(slice: &'a mut [f32]) -> &'a mut [f32]

aka that the output lifetime comes from the input, so all the borrow checking knows they're tied together.

(It means that regardless of what you put in the body, if you try to do something in the body that doesn't meet the signature, it'll fail to compile.)

1 Like

Yes, the signature implies that. (If you write out the elided lifetimes, your function is &'a mut [_] -> &'a mut [_].

1 Like

Thanks, all! Seems like reborrows could certainly use some explanation via the Rust Book. better documentation of reborrowing · Issue #788 · rust-lang/reference · GitHub is a issue asking for this, but it seems to be stuck waiting until the rust team decides how to proceed.

IMO, the project-external text which @vague posted, or something like it, would be a great addition to the Rust Book or one of its friends.

This is due to the expression evaluation order: for assignment into a slice, the left hand side borrow isn't asserted until after the right hand side is done evaluating.

2 Likes

This. For example, this compiles too, even though there isn't any reborrowing:

let mut v: Vec<usize> = vec![1337];
v[0] = v.len();
1 Like

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.