ZyS
ZyS

Reputation: 111

Why the semantic of a moved variable after reinitiating is not move?

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

Answers (1)

E_net4
E_net4

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);

Playground

See also:

Upvotes: 9

Related Questions