Mutably borrowing fields of struct results in an apparent contradiction in compiler error message

Viewed 48

I'm having some trouble understanding the compiler error message for this code:

struct A {
    b: B,
    c: C,
}
struct B {
    f: u32,
}
struct C {
    f: u32,
}

fn foo(b: &mut B) -> &u32 { &b.f }
fn bar(c: &mut C) -> &u32 { &c.f }
fn quux(z: &mut A) -> u32 { z.b.f }

fn baz(a: &mut A) {
    let x = foo(&mut a.b);
    let y = bar(&mut a.c);
    let z = quux(a); // equivalent to: quux(&mut *a)
    println!("{} {}", x, y);
}
error[E0499]: cannot borrow `*a` as mutable more than once at a time
  --> src/main.rs:22:18
   |
20 |     let x = foo(&mut a.b);
   |                 -------- first mutable borrow occurs here
21 |     let y = bar(&mut a.c);
22 |     let z = quux(a);
   |                  ^ second mutable borrow occurs here
23 |     println!("{} {}", x, y);
   |                       - first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` due to previous error

If line 20 is mutably borrowing *a (as the compilation error asserts), then line 21 must also mutably borrow *a (How couldn't it? It's nearly identical to line 20 -- only difference is which field is borrowed), so by my reasoning the code should still fail to compile after deleting line 22; however, the code actually compiles just fine if I delete line 22. The fact that this would then compile seems to contradict the compiler's claim that &mut a.b results in a mutable borrow of *a.

x and y are immutable borrows, so if anything I would expect a compilation error regarding the overlapping scopes of the immutable x/y and the mutable borrow of *a in line 22 (otherwise quux could conceivably mutate a while we're borrowing its fields).

What knowledge am I likely missing that makes this error message confusing to me? If you're not surprised by this message, could you break down how you reason about this?


Some more thinking before I submitted this question:

I suppose the immutable references returned from foo and bar somehow extend the original mutable borrows of a.b and a.c (why that would be necessary, I don't know). If that's the case, maybe the way to see this is that &mut a.b and &mut a.c in turn extend the mutable borrow of *a. The error message says the first mutable borrow of *a occurs on line 20, but maybe the way to see this is:

  • The true first borrow of *a is when when the caller gives us the &mut A.
  • While the compiler suggests there's a mutable borrow of *a on line 20, this isn't exactly the case: the mut &a.b expression borrows (*a).b, but merely extends the existing borrow given to us (as opposed to being a new mutable borrow of *a).
  • The last point, but with respect to mut &a.c.
  • Insert the aforementioned reasoning of "extending borrows" wrt *a.
  • The call to quux then mutably borrows the *a a second time.

So maybe what the compilation error calls the initial borrow of *a is actually reporting the location of the expression responsible for extending the original *a borrow?

2 Answers

Line 20 does not conflict with 21 since they borrow different fields within a and therefore cause no collision.

The issue is that foo and bar return references that inherit the lifetime of the mutable reference. Even though they return immutable references, their lifetimes rely on the lifetime of the mutable borrow. So by requiring they live for a period of time, by extension the mutable borrow must do so as well. The compiler always takes the function signature on face value so it can not downgrade the initial mutable reference. If it did not, it could lead to issues if the function was modified later or if it contained unsafe code that relied on consuming a mutable reference for safety.

Here is a counter example where all of the function signatures remain the same. Since the compiler can not make assumptions on how quux and foo will use their values quux may make the references returned by foo or bar invalid.

struct A {
    b: B,
    c: C,
}
struct B {
    f: Option<u32>,
}
struct C {
    f: u32,
}


fn foo(b: &mut B) -> &u32 { 
    b.f.as_ref().unwrap()
}

fn bar(c: &mut C) -> &u32 { &c.f }

fn quux(z: &mut A) -> u32 {
    z.b.f = None;
    z.c.f
}

fn baz(a: &mut A) {
    let x = foo(&mut a.b);
    let y = bar(&mut a.c);
    let z = quux(a);
    println!("{} {}", x, y);
}

There are two issues here. First: why does let x = foo(&mut a.b); prevent you from doing let z = quux(a);, and second why does let x = foo(&mut a.b); not prevent you from doing let y = bar(&mut a.c);?

Why does let x = foo(&mut a.b); prevent you from doing let z = quux(a);?

This is due to the lifetime elision rules. You declared foo as:

fn foo(b: &mut B) -> &u32

Since you use references there must be a lifetime, but since you didn't specify one yourself the compiler assumes that all references have the same lifetime. In other words, the compiler rewrites your code as:

fn foo<'a>(b: &'a mut B) -> &'a u32

Which means that the returned value has the same lifetime as the mutable borrow, so the mutable borrow lasts until the println where you use the return value x, which prevents other borrows of a.b or a as a whole.

But then why does let x = foo(&mut a.b); not prevent you from doing let y = bar(&mut a.c);?

That's because the compiler is smart enough to realize that a.b and a.c are disjoint parts of a, which can be borrowed without conflict (in the same way that you can borrow different local variables simultaneously).

Related