dacker
dacker

Reputation: 457

Variable binding: moving a &mut or borrowing the referent?

This code fails as expected at let c = a; with compile error "use of moved value: a":

fn main() {
    let a: &mut i32 = &mut 0;
    let b = a;
    let c = a;
}

a is moved into b and is no longer available for an assignment to c. So far, so good.

However, if I just annotate b's type and leave everything else alone:

fn main() {
    let a: &mut i32 = &mut 0;
    let b: &mut i32 = a;
    let c = a;
}

the code fails again at let c = a;

But this time with a very different error message: "cannot move out of a because it is borrowed ... borrow of *a occurs here: let b: &mut i32 = a;"

So, if I just annotate b's type: no move of a into b, but instead a "re"-borrow of *a?

What am I missing?

Cheers.

Upvotes: 15

Views: 1558

Answers (2)

Jmb
Jmb

Reputation: 23294

To complement @Levans's answer on the specific question "Why does annotating the type change the behaviour?":

When you don't write the type, the compiler performs a simple move. When you do put the type, the let statement becomes a coercion site as documented in "Coercion sites":

Possible coercion sites are:

  • let statements where an explicit type is given.

In the present case the compiler performs a reborrow coercion, which is a special case of coercion going from &mut to &mut, as explained in this issue comment on GitHub.

Note that reborrowing in general and reborrow coercion in particular are currently poorly documented. There is an open issue on the Rust Reference to improve that point.

Upvotes: 3

Levans
Levans

Reputation: 15002

So, if I just annotate b's type: no move of a into b, but instead a "re"-borrow of *a?

What am I missing?

Absolutely nothing, as in this case these two operations are semantically very similar (and equivalent if a and b belong to the same scope).

  • Either you move the reference a into b, making a a moved value, and no longer available.
  • Either you reborrow *a in b, making a unusable as long as b is in scope.

The second case is less definitive than the first, you can show this by putting the line defining b into a sub-scope.

This example won't compile because a is moved:

fn main() {
    let a: &mut i32 = &mut 0;
    { let b = a; }
    let c = a;
}

But this one will, because once b goes out of scope a is unlocked:

fn main() {
    let a: &mut i32 = &mut 0;
    { let b = &mut *a; }
    let c = a;
}

Now, to the question "Why does annotating the type of b change the behavior ?", my guess would be:

  • When there is no type annotation, the operation is a simple and straightforward move. Nothing is needed to be checked.
  • When there is a type annotation, a conversion may be needed (casting a &mut _ into a &_, or transforming a simple reference into a reference to a trait object). So the compiler opts for a re-borrow of the value, rather than a move.

For example, this code is perflectly valid:

fn main() {
    let a: &mut i32 = &mut 0;
    let b: &i32 = a;
}

and here moving a into b would not make any sense, as they are of different type. Still this code compiles: b simply re-borrows *a, and the value won't be mutably available through a as long as b is in scope.

Upvotes: 10

Related Questions