Why is the mutable reference not moved here?
Asked Answered
I

4

26

I was under the impression that mutable references (i.e. &mut T) are always moved. That makes perfect sense, since they allow exclusive mutable access. In the following piece of code I assign a mutable reference to another mutable reference and the original is moved. As a result I cannot use the original any more:

let mut value = 900;
let r_original = &mut value;
let r_new = r_original;
*r_original; // error: use of moved value *r_original

If I have a function like this:

fn make_move(_: &mut i32) {
}

and modify my original example to look like this:

let mut value = 900;
let r_original = &mut value;
make_move(r_original);
*r_original; // no complain

I would expect that the mutable reference r_original is moved when I call the function make_move with it. However that does not happen. I am still able to use the reference after the call.

If I use a generic function make_move_gen:

fn make_move_gen<T>(_: T) {
}

and call it like this:

let mut value = 900;
let r_original = &mut value;
make_move_gen(r_original);
*r_original; // error: use of moved value *r_original

The reference is moved again and therefore the program behaves as I would expect. Why is the reference not moved when calling the function make_move?

Code example

Inhuman answered 22/8, 2015 at 9:15 Comment(2)
Explicitly instantiating (make_move::<&mut i32>(r_original);) works like the original function (no move). Fascinating; I'd assume borrow checking is happening before type inference.Mahayana
From dacker's answer I assume that is the case: Explicitly annotating the type to be a mutable reference triggers a re-borrow of the contents instead of a move, leaving the original reference to be usable again once the new reference (here in make_move's scope) goes out of scope.Inhuman
M
17

There might actually be a good reason for this.

&mut T isn't actually a type: all borrows are parametrized by some (potentially inexpressible) lifetime.

When one writes

fn move_try(val: &mut ()) {
    { let new = val; }
    *val
}

fn main() {
    move_try(&mut ());
}

the type inference engine infers typeof new == typeof val, so they share the original lifetime. This means the borrow from new does not end until the borrow from val does.

This means it's equivalent to

fn move_try<'a>(val: &'a mut ()) {
    { let new: &'a mut _ = val; }
    *val
}

fn main() {
    move_try(&mut ());
}

However, when you write

fn move_try(val: &mut ()) {
    { let new: &mut _ = val; }
    *val
}

fn main() {
    move_try(&mut ());
}

a cast happens - the same kind of thing that lets you cast away pointer mutability. This means that the lifetime is some (seemingly unspecifiable) 'b < 'a. This involves a cast, and thus a reborrow, and so the reborrow is able to fall out of scope.

An always-reborrow rule would probably be nicer, but explicit declaration isn't too problematic.

Mahayana answered 22/8, 2015 at 16:17 Comment(6)
I never thought of it that way. However it seems logical. By "cast away pointer mutability" you mean getting a shared reference from an mutable reference? Like let r: &i32 = r_mut; where r_mut is &mut i32.Inhuman
Nice explanation. This really makes sense. My guess regarding 'b would be: it's the intersection of 'a which encompasses move_try's body and the nested scope where new is defined; so 'b would be this nested scope at the end of which new's borrow ends.Goodrum
As per the explaination, first 2 snippets seem equivalent but they show different error messages Playground. First snippet says value moved whereas second snippet says still borrowed. Did I misunderstood something?Shelba
@Mihir Technically let new: &'a mut _ = val; is a reborrow, it's just a no-op reborrow since the lifetimes are identical.Mahayana
@Veedrac, it makes sense when you say "&mut T isn't actually a type". Although, I am curious, let new = val moves the mutable reference. Then if we had to explicitly annotate new wanting data to be moved from val to new, how would be do that? let new: _ = val. What would be the exact type of _?Shelba
@Mihir I don't think you can, unless you have the whole type in a type variable.Mahayana
G
5

I asked something along those lines here.

It seems that in some (many?) cases, instead of a move, a re-borrow takes place. Memory safety is not violated, only the "moved" value is still around. I could not find any docs on that behavior either.

@Levans opened a github issue here, although I'm not entirely convinced this is just a doc issue: dependably moving out of a &mut reference seems central to Rust's approach of ownership.

Goodrum answered 22/8, 2015 at 10:12 Comment(0)
E
4

It's implicit reborrow. It's a topic not well documented.

This question has already been answered pretty well:

  1. how implicit reborrow works
  2. how reborrow works along with borrow split
Endanger answered 25/9, 2021 at 21:46 Comment(0)
V
1

If I tweak the generic one a bit, it would not complain either

fn make_move_gen<T>(_: &mut T) {
}

or

let _ = *r_original; 
Vulgarian answered 18/12, 2021 at 5:19 Comment(4)
This doesn't really help answer the "why" behind this behavior. And the second example is just dereferencing and not really relevant here.Hamburg
@Hamburg by second example, I meant if I changed "*r_original;" in the question's code block to "let _ = *r_original;", it would compile. I was playing around and looks to me that when I do "let _ = r_original;", it would not do de-ref and thus it will not complain either even though the value has been moved indeed.Vulgarian
oh that's interesting! It does remove the error. Sorry for misunderstanding, I've changed my vote.Hamburg
thanks for the comment though, I have long been a pure consumer and learning to contribute a bit back :)Vulgarian

© 2022 - 2024 — McMap. All rights reserved.