How does this example not break single-writer rule?

How does the allowance of two mutable refs to the same value (if that's what this is) not break the single-writer rule?
Compiles for non-primitive types too.

struct A<'a> {
    val: &'a mut u32,
}
impl<'a> A<'a> {
    fn as_mut(&mut self) -> &mut u32 {
        self.val
    }
}
fn test(val: &mut u32) {
    let mut a = A { val };
    let mut_val1 = a.as_mut();
    let mut_val2 = a.as_mut();
}

The compiler knows that mut_val1 is not used by the time mut_val2 is declared.

It is very reasonable to ask this question, in fact, for a long time, the rust compiler would have agreed with your assertion:

error[E0499]: cannot borrow `a` as mutable more than once at a time
  --> <source>:12:20
   |
11 |     let mut_val1 = a.as_mut();
   |                    - first mutable borrow occurs here
12 |     let mut_val2 = a.as_mut();
   |                    ^ second mutable borrow occurs here
13 | }
   | - first borrow ends here

But by now, with a capability called “non-lexical lifetimes”, this code is allowed. The idea is to behave, for the purposes of borrow-checking, as-if the scope of local variables like mut_val1 is shorter than it actually is. The mut_val1 variable is technically only dropped after the second a.as_mut(), but we can behave as-if it was dropped because

  • dropping &mut A<'_> is a no-op, so (behaving as-if we’re) dropping earlier doesn’t change observable behavior
  • the variable mut_val1 is no longer used by the time of the second a.as_mut() call, so behaving as-if we’re dropping it earlier will not introduce any use-after-free problems

“used” in this case of course refers not only to direct mentions, but also some transitive usage by using other re-borrows. E.g. while

fn test(val: &mut u32) {
    let mut a = A { val };
    let mut_val1 = a.as_mut();
    let borrow = &mut_val1;
    let mut_val2 = a.as_mut();
}

works, in

fn test(val: &mut u32) {
    let mut a = A { val };
    let mut_val1 = a.as_mut();
    let borrow = &mut_val1;
    let mut_val2 = a.as_mut();
    println!("{borrow}");
}

the usage of borrow also effectively is, indirectly, a usage of mut_val1:

error[E0499]: cannot borrow `a` as mutable more than once at a time
  --> src/lib.rs:13:20
   |
11 |     let mut_val1 = a.as_mut();
   |                    ---------- first mutable borrow occurs here
12 |     let borrow = &mut_val1;
13 |     let mut_val2 = a.as_mut();
   |                    ^^^^^^^^^^ second mutable borrow occurs here
14 |     println!("{borrow}");
   |                ------ first borrow later used here

note how the “later used here” of the borrow created when defining mut_val1 is pointing to a usage of borrow.

Similarly, the condition about destructors needing to be no-ops is also a special-case of the general “usage” rule, if you will, since a custom destructor (i.e. Drop impl) would arguably use the variable.

In case of custom types like yours, this has the observable consequence that adding a Drop impl for A<'_> will make this code fail to compile:

// compiles
struct A<'a> {
    val: &'a mut u32,
}
impl<'a> A<'a> {
    fn as_mut(&mut self) -> &mut u32 {
        self.val
    }
}
fn test(val: &mut u32) {
    let a = A { val };
    let b = A { val };
}
// fails
struct A<'a> {
    val: &'a mut u32,
}
impl<'a> A<'a> {
    fn as_mut(&mut self) -> &mut u32 {
        self.val
    }
}
impl Drop for A<'_> {
    fn drop(&mut self) {}
}
fn test(val: &mut u32) {
    let a = A { val };
    let b = A { val };
}
2 Likes

Makes a lot more sense now, thank you.
This smart compiler detection didn't occur to me because it rejects the even simpler form:

let mut a = A { val };
let mut mut_val1 = a.val;
let mut mut_val2 = a.val; // error: use of moved value: `a.val`

FYI, the reason why this “simpler form”-example doesn’t compile has to do with some niche rules as to when implicit re-borrowing does or doesn’t happen, which are related to / interacting with type inference. In my opinion, the situation should be improved eventually, but there’s probably relevant effects in niche cases to consider… The code example you show does compile if type annotations are added as follows:

let mut a = A { val };
let mut_val1: &mut _ = a.val;
let mut_val2: &mut _ = a.val;

alternatively, one could re-borrow explicitly by writing &mut *a.val instead of a.val.

let mut a = A { val };
let mut_val1 = &mut *a.val;
let mut_val2 = &mut *a.val;

As you can see in the error message, in case these workarounds aren’t taken, the error message is nothing about borrow checking, but merely that a value has been moved; but moving a &mut _ reference can always be avoided by using re-borrowing instead, in which case the variable/field stays initialized / not-moved-out-of, but the borrow checker now makes sure that the reference stays unique / not-shared.

I see, so re-borrowing the first but not the second is also an option.

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.