1

This code compiles (playground link):

use std::collections::HashMap;

fn main() {
    let mut h = HashMap::<char, Vec<i32>>::new();
    h.insert('a', vec![0]);
    
    let first_borrow = h.get_mut(&'a').unwrap();
    first_borrow.push(1);
    let second_borrow = h.get_mut(&'a').unwrap();
    second_borrow.push(2);
}

Changing the order of the code using the borrows (the push() calls)...

let first_borrow = h.get_mut(&'a').unwrap();
let second_borrow = h.get_mut(&'a').unwrap();
first_borrow.push(1);
second_borrow.push(2);

...makes it not compile:

error[E0499]: cannot borrow `h` as mutable more than once at a time
 --> src/main.rs:8:25
  |
7 |     let first_borrow = h.get_mut(&'a').unwrap();
  |                        - first mutable borrow occurs here
8 |     let second_borrow = h.get_mut(&'a').unwrap();
  |                         ^ second mutable borrow occurs here
9 |     first_borrow.push(1);
  |     ------------ first borrow later used here

Moreover, using first_borrow past the instantiation of second_borrow also doesn't compile:

let first_borrow = h.get_mut(&'a').unwrap();
first_borrow.push(1);
let second_borrow = h.get_mut(&'a').unwrap();
second_borrow.push(2);
    
// ...
    
first_borrow.push(1);

This is surprising given what the documentation seems to say about scopes. In the code that compiles, why don't we have two mutable borrows there, too?

In the example that compiles, does Rust see that, after let second_borrow = ..., there's no more mention of first_borrow anywhere, so it unborrows the mutable borrow of first_borrow and thus retains a single borrow across the whole scope of main()?!

6equj5
  • 91
  • 1
  • 12
  • 2
    What exactly is it "the documentation seems to say about scopes"? In older versions of Rust (pre-1.26, IIRC) some code like this would not have compiled. It does compile now. Are you possibly reading some out-of-date documentation? – trent Oct 12 '20 at 03:24
  • @CoronA Nearly! Thanks. My case only differs in that my mutable borrows seem bound to objects (e.g., `first_borrow`) that are still right there in scope while, in that question, they're solely in function calls in the scope. The cases may be technically equivalent, but mine is more counterintuitive to me. – 6equj5 Oct 12 '20 at 04:48
  • 1
    I recommend reading the [current documentation](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html) which explains how borrows behave. – Herohtar Oct 12 '20 at 06:53
  • The current docs do indeed explain this: "Note that a reference’s scope starts from where it is introduced and continues through the **last time that reference is used**." Unfortunately, that's on the tail end of many scoping examples that use `{` `}` braces to denote scope, which sustained my confusion for a bit. – 6equj5 Oct 12 '20 at 17:37

1 Answers1

2

In the code that compiles, why don't we have two mutable borrows there, too?

The short answer is that two mutable borrows of the same piece of data cannot be in scope simultaneously, and the first example does not violate that restriction. Note that this is a corollary of the "one big restriction" of mutable references which is "you can have only one mutable reference to a particular piece of data in a particular scope." See the References and Borrowing section in The Rust Programming Language.

You first example compiles because first_borrow goes out of scope before second_borrow comes into scope. "Going out of scope" is synonymous with a variable not being referenced for the remainder of a scope. I don't know the low level details, but here is how I think of that example.

    // first_borrow comes into scope
    let first_borrow = h.get_mut(&'a').unwrap();
    first_borrow.push(1);
    // first_borrow goes out of scope
    // second_borrow comes into scope
    let second_borrow = h.get_mut(&'a').unwrap();
    second_borrow.push(2);
    // second_borrow goes out of scope

For your second example that does not compile, we can see that the scopes of first_borrow and second_borrow cross.

    // first_borrow comes into scope
    let first_borrow = h.get_mut(&'a').unwrap();
    // second_borrow comes into scope
    let second_borrow = h.get_mut(&'a').unwrap();
    // !!! both first_borrow and second_borrow are in scope now !!!
    first_borrow.push(1);
    // first_borrow goes out of scope
    second_borrow.push(2);
    // second_borrow goes out of scope

In the example that compiles, does Rust see that, after let second_borrow = ..., there's no more mention of first_borrow anywhere, so it unborrows the mutable borrow of first_borrow and thus retains a single borrow across the whole scope of main()?!

Effectively, yes. I don't think of this as unborrowing though. As worded above, I believe the term is that first_borrow goes out of scope.

For example, you could have written the first example like this.

    {
        let first_borrow = h.get_mut(&'a').unwrap();
        first_borrow.push(1);
    }
    {
        let second_borrow = h.get_mut(&'a').unwrap();
        second_borrow.push(2);
    }

And of course the second example cannot be written in such a way because the borrows cross each other.

  • 2
    The feature that causes a variable to "go out of scope" where it is last used -- rather than at the end of the containing block -- is called "non-lexical lifetimes." https://stackoverflow.com/questions/50251487/what-are-non-lexical-lifetimes – AmigoNico Oct 12 '20 at 04:48
  • Earlier, I was reading some [old docs](https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html#mut-references) that date back to when using the braces for scope definition wasn't optional, like it is now! That's also around the time I first learned about borrowing, and my confusion stemmed from disbelief that something like `first_borrow`'s scope can end before `main()`'s closing `}`. – 6equj5 Oct 12 '20 at 04:55
  • @AmigoNico Technically correct, however, since the new borrow checker does not use scope at all in its analysis, I find it unhelpful to try to describe this feature in terms of how it's different from what the "old" borrow checker did -- that kind of explanation only makes sense to people who were already familiar with *lexical* lifetimes. It's an interesting historical note, but it has no explanatory power. – trent Oct 12 '20 at 05:12