10

Why doesn't the following code compile (playground):

use std::collections::HashMap;

fn main() {
    let mut h: HashMap<u32, u32> = HashMap::new();
    h.insert(0, 0);
    h.insert(1, h.remove(&0).unwrap());
}

The borrow checker complains that:

error[E0499]: cannot borrow `h` as mutable more than once at a time
 --> src/main.rs:6:17
  |
6 |     h.insert(1, h.remove(&0).unwrap());
  |     - ------    ^ second mutable borrow occurs here
  |     | |
  |     | first borrow later used by call
  |     first mutable borrow occurs here

The code is safe, however, and an almost mechanical transformation of the last line makes it compile (playground):

    //h.insert(1, h.remove(&0).unwrap());
    let x = h.remove(&0).unwrap();
    h.insert(1, x);

It was my understanding that this kind of issue got resolved with non-lexical lifetimes. This question is an example, and there are many others.

Is there some subtlety that makes the first variant incorrect after all, so Rust is correct to refuse it? Or is the NLL feature still not finished in all cases?

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 1
    I disagree that (a) having a complete error message, so that the post is easier found by search engines, or (b) having a complete sentence in the title, so that people can easily identify if the post is relevant to their question are "trivial" or "stylistic", but whatever floats your boat. – Shepmaster Nov 24 '20 at 18:02
  • 1
    Shepmaster/Stargateur I think your edits would go down a lot better if you restricted yourself to helpful edits (i.e. the error message), rather than just rewording things in a way that you think is better style (e.g. removing "however") because the question writer may not agree. – Timmmm Nov 24 '20 at 19:38
  • @Timmmm if that were the case, I would hope the author would add back the "however" instead of rolling back all the edits. Rolling back the edits (and not manually re-applying some of them) indicates that _every single edit_ was bad. I **only** make edits I feel are "helpful" so I'm not sure how actionable your advice is ;-). My editing audience is the thousands (millions, I hope!) of people that might want to find and read this question in the years to come, not the author of the post. – Shepmaster Nov 24 '20 at 19:42
  • Also note that without the `@` no one gets notified; I only noticed this because I was adding the error message to the _answer_, which is a pretty bad place for it, but one I have the agency to change. – Shepmaster Nov 24 '20 at 19:45
  • 1
    @Shepmaster I've added the full error message to the question as, looking at it once again, the omitted parts do seems useful for understanding the issue. – user4815162342 Nov 24 '20 at 21:40
  • 1
    Geeze. We're not talking about getting missile launch codes right here, are we? I've got a few spare chill pills if anyone is in need. What I do when my edits are rejected is put a note in my calendar (just the URL actually) with a reminder set. I come back later, where I can benefit by both a fresh eye and the sum total of all edits made "during the battle" as it were. I don't think I've ever had edits I've made a week after the Question arose rejected. – CryptoFool Nov 24 '20 at 22:38
  • 1
    @Stargateur Ok, now I'm beginning to understand why you took offense and I do owe you an apology. I didn't carefully examine your edit, which looked to me like another rollback to Shepmaster's edit I had already rejected in some sort of edit war. Had I noticed that you were careful to remove the title edit, I would have acted differently myself. Sorry about that. – user4815162342 Nov 24 '20 at 22:51
  • 1
    guess we have both miss a thing, we have an understanding, I apologize too so. – Stargateur Nov 24 '20 at 22:53

2 Answers2

6

Your question also applies for a related case that may be more surprising — having the method call require &self when the method argument requires &mut self:

use std::collections::HashMap;

fn main() {
    let mut h: HashMap<u32, u32> = HashMap::new();
    h.insert(0, 0);
    h.contains_key(&h.remove(&0).unwrap());
}

The Rust borrow checker uses what it calls two-phase borrows. An edited transcription of a chat I had with Niko Matsakis:

The idea of two-phase borrows is that the outer &mut is treated like an & borrow until it is actually used, more or less. This makes it compatible with an inner & because two & mix, but it is not compatible with an inner &mut.

If we wanted to support that, we'd have had to add a new kind of borrow -- i.e., an "unactivated" &mut wouldn't act like an &, it would act like something else (&const, maybe... "somebody else can mutate")

It's less clear that this is OK and it seemed to add more concepts so we opted not to support it.

As you stated, this is safe because the inner borrow is completed before the outer borrow starts, but actually recognizing that in the compiler is overly complex at this point in time.

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Thanks for providing what's effectively inside information. :) It's still not quite clear if this feature will never be supported, or if it's just infeasible to support it in the current borrow checker implementation. I'd normally expect the latter, but Niko's phrasing "we opted not to support it" leaves it a bit ambiguous. Ideally these kinds of decisions would be documented somewhere, if nothing else to allow an independent implementation of the compiler. But until such documentation is available, this answer is probably the closest we'll get to answering the question, so I'll accept it. – user4815162342 Nov 25 '20 at 09:46
  • @user4815162342 My understanding of the situation is that it's possible, but any changes to the borrow checker can involve introducing subtle bugs that lead to memory safety. Especially adding a new kind of reference (the hypothetical `&const`) means that any place that needs to consider that could be broken. I read it as "not worth the potential problems at this point in time", especially considering the mechanical workaround of introducing a temporary variable. – Shepmaster Nov 25 '20 at 14:46
  • I'm not sure I understood what `&const` refers to. Is that a hypothetical new language construct which would allow smarter borrow checks, or is it a hypothetical new abstraction only present inside the compiler? If it's the latter, then I could envision it being implementing in some form one day, perhaps to enable some other more important feature. If it's the former, then I don't get the connection to the code in my question (and the code in your answer), which doesn't/shouldn't require a new type of borrow. – user4815162342 Nov 25 '20 at 21:12
-2

The Rust compiler first evaluates the calling object, then the arguments passed to it. That's why it first borrows the h.insert, then h.remove. Since the h is already borrowed mutably for insert, it denies the second borrow for remove.

This situation is not changed when using Polonius, a next-generation borrow checker. You can try it yourself with the nightly compiler:
RUSTFLAGS=-Zpolonius cargo +nightly run

The order of evaluation is similar in C++: https://riptutorial.com/cplusplus/example/19369/evaluation-order-of-function-arguments

Fazer
  • 32
  • 5
  • If `h` is borrowed mutably first, then why does `v.remove(v.len() - 1)` [compile](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=982a558f328b8b01d019720078c10229)? `v.len()` requires a shared reference which shouldn't be allowed while an exclusive reference exists. – user4815162342 Mar 15 '20 at 16:05
  • `v.remove(v.len() - 1)` works because NLL detects it as a valid case. However NLL does not support the original example with double mutable borrowing. – Fazer Mar 22 '20 at 20:51
  • Is this is lack of support an oversight or is the feature simply planned for a future NLL version? Or, is there a more fundamental reason why this wouldn't work with NLL in any form, and is therefore not planned at all? – user4815162342 Mar 22 '20 at 20:59