Reputation: 111
The minimal example is this (Playground):
#[derive(Debug)]
struct Node {
id: usize,
}
fn main() {
let mut node = Node { id: 0 };
let mut lastnode = &mut node;
let mut last = lastnode; // move
(*last).id = 1;
println!("{:?}", last);
//println!("{:?}", lastnode);
let mut node2 = Node { id: 2 };
lastnode = &mut node2;
last = lastnode; // doesn't move to "last"
println!("{:p}", last); // "last" and "lastnode" point to the same address
println!("{:p}", lastnode);
}
Why does the first last = lastnode
cause ownership to be moved, but the second last = lastnode
seems like it only follows borrow semantics?
Upvotes: 7
Views: 120
Reputation: 30052
This is a rather entangled case of the compiler's borrowing semantics, combined with non-lexical lifetimes. Let's go through this step by step.
let mut node = Node { id: 0 };
let mut lastnode = &mut node;
let mut last = lastnode; // move
This line indeed moves lastnode
into last
, which can be evidenced by uncommenting the line which comes after:
//println!("{:?}", lastnode);
Should we uncomment this one, the error from the compiler would be:
error[E0382]: borrow of moved value: `lastnode`
--> src/main.rs:12:22
|
8 | let mut lastnode = &mut node;
| ------------ move occurs because `lastnode` has type `&mut Node`, which does not implement the `Copy` trait
9 | let mut last = lastnode; // move
| -------- value moved here
...
12 | println!("{:p}", lastnode);
| ^^^^^^^^ value borrowed here after move
For what it's worth, it is possible to make this code work by explicitly reborrowing lastnode
, as below.
let mut last = &mut *lastnode; // move no more!
With a reborrow, last
will return the borrow to lastnode
once the reference currently bound to last
is not used anymore. There is only a reassignment done later, which binds it to another reference and so does not result in any conflict.
What may be the most intriguing part is that things seem to just work in the subsequent steps, even though the structure and order of operations is the same.
let mut node2 = Node { id: 2 };
lastnode = &mut node2;
last = lastnode; // doesn't move to "last"
println!("{:p}", last); // "last" and "lastnode" point to the same address
println!("{:p}", lastnode);
If lastnode
had been moved into last
, then the compiler wouldn't have let us call println!("{:p}", lastnode)
. last = lastnode
was an implicit reborrow, which was dropped just before the last line, thanks to non-lexical lifetimes.
Considering that implicit reborrowing is a poorly documented feature of the compiler, it comes down to an edge case where the current compiler did not know to reborrow a mutable reference implicitly to satisfy the given code. Or in other words, the current version of the compiler just managed to make the second part of the code work, but not the first one.
The underlying reason for this may well be an implementation detail on how the two variables were typed. While lastnode
was always assigned a mutable reference to nodes, last
was defined through multiple assignments to lastnode
. And when more than one assignment takes place, this has invited the compiler to give it a reborrow the second time, possibly to fulfill the lifetime it was coerced to. In any case, this is not tied to any invariant of the language, and could change in future versions.
For completeness, here is another minimal example of the implicit reborrowing in action, where adding a redundant assignment makes the code compile.
#[derive(Debug)]
struct Node;
let mut node = Node;
let mut node2 = Node;
let lastnode;
let mut last;
last = &mut node; // <-- remove this to fail
lastnode = &mut node2;
last = lastnode;
println!("{:p}", last);
println!("{:p}", lastnode);
See also:
Upvotes: 9