2

I'm having problem with some Rust code where I'm being allowed to borrow something as mutable more than once on certain conditions (first confusing part), but not others.

I've written the following example to illustrate: (Playground)

struct NoLifetime {}
struct WithLifetime <'a> {
    pub field: &'a i32
}

fn main() {
    let mut some_val = NoLifetime {};
    borrow_mut_function(&mut some_val);
    borrow_mut_function(&mut some_val); // Borrowing as mutable for the second time.

    let num = 5;
    let mut life_val = WithLifetime { field: &num };
    borrow_lifetime(&mut life_val);
    borrow_lifetime(&mut life_val); // Borrowing as mutable for the second time.

    let num_again = borrow_lifetime(&mut life_val); // Borrow, assign lifetime result
    borrow_lifetime(&mut life_val); // Compiler: cannot borrow `life_val` as mutable more than once
}

fn borrow_mut_function(val_in: &mut NoLifetime) -> String {
    "abc".to_string()
}
fn borrow_lifetime<'a>(val_in: &'a mut WithLifetime) -> &'a i32 {
    val_in.field
}

If you see, I can borrow both some_val, and life_val as mutable more than once. However, after assigning the return value of borrow_lifetime, I can no longer borrow.

My questions are the following:

  1. From 'The Rules' about Borrowing in the Rust Book, I'm supposed to have 'exactly one mutable reference' in scope to the same value. However, in the code above I'm borrowing as mutable every time I call a borrow_ function.
  2. Why is the same type of borrowing not allowed when I have a function that returns something with the same lifetime as the parameter, and I assign that parameter.

Any help would be appreciated. I imagine what is happening here is that I am misunderstanding what 'borrowing as mutable' really means, and when to determine that something is being borrowed as mutable.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
chamakits
  • 1,865
  • 1
  • 17
  • 26

2 Answers2

9

Rust now accepts this code, due to non-lexical borrows.

With non-lexical borrows, a borrow ends on the last use of the variable (essentially), with a note that a destructor is a use, and destructors run at the end of the scope.

Thus, as the compiler realizes that:

  • num_again is never used
  • num_again does not have a specific destructor (no Drop implementation)

it decides that its borrow ends on the very line it starts, rather than at the end of the lexical scope, and thus life_val is free to be used again.


Chris already gave the gist of it, but I think it is worth explaining further.

There are 2 ways to transfer ownership in Rust:

  • moving is a permanent transfer
  • borrowing is a temporary transfer, ownership is expected to be returned

Rust, like many other languages, models time passing using a stack of lexical scopes. As a result, for now, a borrow starts where it is created and extend until the end of its scope.


Historical behavior, prior to non-lexical borrows:

Thus, the question of when a borrow ends used to be akin to asking what scope the borrow was created in.

Let's review your example with numbered lines:

fn main() {
    let mut some_val = NoLifetime {};                // 1
    borrow_mut_function(&mut some_val);              // 2
    borrow_mut_function(&mut some_val);              // 3
                                                     // 
    let num = 5;                                     // 4
    let mut life_val = WithLifetime { field: &num }; // 5
    borrow_lifetime(&mut life_val);                  // 6
    borrow_lifetime(&mut life_val);                  // 7
                                                     //
    let num_again = borrow_lifetime(&mut life_val);  // 8
    borrow_lifetime(&mut life_val);                  // 9
}

When a function is called, the argument was borrowed:

  • at least for the duration of the function call.
  • up to the moment the result was dropped, if the result shared a lifetime with the argument.

So, let's look at this:

  • on line (2) and (3) you call borrow_mut_function which returns a String: the result does not share any lifetime with the argument, so the argument is only borrowed for the lifetime of the function call.

  • on line (6) and (7) you call borrow_lifetime which returns a &'a i32: the result shares a lifetime with the argument, so the argument is borrowed until the end of the scope of the result... which is immediately since the result is not used.

  • on line (8) you call borrow_lifetime which returns a &'a i32 and you assign the result to num_again: the result shares a lifetime with the argument, so the argument was borrowed until the end of the scope of num_again.

  • on line (9) you call borrow_lifetime however its argument is still borrow by num_again so the call is illegal.

That's it, this was how Rust worked back then.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • As of rust 1.65.0 this code compiles. The compiler recognizes that num_again is not used. – Nobody May 19 '23 at 18:42
  • 1
    @Nobody: Answer updated, as it used to hint at the bottom, this is non-lexical borrows at play. I've moved that to the top, and reworded the historical behavior with the past tense. – Matthieu M. May 20 '23 at 10:31
5

This is about the scope of the borrow, and whether you keep the borrow alive. In most of the above calls, some_val is borrowed during the function call, but returned afterwards when the function returns.

In the exception case:

let num_again = borrow_lifetime(&mut life_val); //Borrow, assign lifetime result

You're borrowing life_val during the call to borrow_lifetime, but since the return value has the same lifetime as the parameter ('a), the borrow's scope is extended to include the lifetime of num_again, ie until the end of the function. It would be unsafe to borrow life_val again, since num_again is still a reference into it.

Chris Emerson
  • 13,041
  • 3
  • 44
  • 66