Why we Can't Borrow a Mutable Object More Than Once as a Function Argument

Hi,
I'm starting our with rust, and stumbled upon a behavior of the borrow checker that I don't understand. I'm hoping this is the right place to understand why this happens (I understand what happened). Suppose I have the following code:

struct Bar {}

impl Bar {
    fn foo(&mut self, x: u8) -> u8 {
        x
    }
    
    fn faz(&mut self) -> u8 {
        0
    }
}

fn main() {
    let mut bar = Bar{};
    println!("{}", bar.foo(bar.faz()));
}

This code fails to compile, with the compiler giving a perfect reason:

error[E0499]: cannot borrow `bar` as mutable more than once at a time
  --> src/main.rs:15:28
   |
15 |     println!("{}", bar.foo(bar.faz()));
   |                    --- --- ^^^ second mutable borrow occurs here
   |                    |   |
   |                    |   first borrow later used by call
   |                    first mutable borrow occurs here
   |
help: try adding a local storing this argument...
  --> src/main.rs:15:28
   |
15 |     println!("{}", bar.foo(bar.faz()));
   |                            ^^^^^^^^^
help: ...and then using that local as the argument to this call
  --> src/main.rs:15:20
   |
15 |     println!("{}", bar.foo(bar.faz()));
   |                    ^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0499`.

The error also gives us a fix - introducing a local variable:

struct Bar {}

impl Bar {
    fn foo(&mut self, x: u8) -> u8 {
        x
    }
    
    fn faz(&mut self) -> u8 {
        0
    }
}

fn main() {
    let mut bar = Bar{};
    let x = bar.faz();
    println!("{}", bar.foo(x));
}

Now I'm getting what happened here, but not why the compiler didn't not introduce this local variable himself (manually writing a variable only for one time use seems a bit clunky to me). Is there any particular reason why the compiler could not solve this issue with the first code (internally introducing the variable)?

This is my first time posting here, so I'm hoping this is the right place. Thanks!

You are in the right place!

The reason is the order of operations.

Imagine a case like this, where bar was an arbitrary expression.

struct Baz {
    bar: Bar,
}

impl Baz {
    fn get_bar(&mut self, x: i32) -> &mut Bar {
        println!("{}", x);
        &mut self.bar
    }
}

Now compare

baz.get_bar(1).foo(baz.get_bar(2).faz())

with

let x = baz.get_bar(2).faz();
baz.get_bar(1).foo(x)

The first example would print 1 and 2, while the second prints 2 then 1.

You could say that the compiler should also create a temporary for the other value too:

let y = baz.get_bar(1);
let x = baz.get_bar(2).faz();
y.foo(x);

...but this wouldn't work either, because now we want to get a mutable (unique) reference to baz to create x, but the borrow to baz is already held by y.

The only remaining option would be to change the order of operations in method calls so that the arguments are evaluated before the receiver, but that would be very confusing, especially when the receiver is a chained expression.


Do note though, that if faz took a shared reference and the receiver is a sufficiently simple expression, then

bar.foo(bar.faz())

does actually work!

This does require a bit of compiler magic, but the idea is that if the receiver bar does not have side effects, then the mutable reference can have a dormant state, where it can be aliased with shared references in the arguments, before getting activated for the call to foo.

This behavior is referred to as two-phase borrows.

5 Likes