15

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.

dacker
  • 457
  • 2
  • 10

2 Answers2

9

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.

Community
  • 1
  • 1
Levans
  • 14,196
  • 3
  • 49
  • 53
  • Thx Levans for the answer. I actually checked the subscope variant before to verify that a borrow of `*a` actually took place. Could have been just a false compiler message, after all. Regarding your guess, I would feel very uncomfortable if anytime I use type annotation I would have to be prepared for some weird re-write of the obvious meaning: `let a = b;` should always be just a move or a copy, type annotated or not. – dacker May 29 '15 at 22:32
  • @dacker well, keep in mind that this behavior can *only* be reached with `&mut` references, in which case a re-borrow is simply a copy which respects the borrow rules. – Levans May 30 '15 at 06:52
  • But wasn't that a big point in all the docs I've read so far: &muts are always moved in these cases? NB: the same behavior as above happens with assignments to type annotated &muts as well. – dacker May 30 '15 at 10:53
  • Unless you are playing with nested scopes, re-borrowing or moving a `&mut` reference are effectively the same thing, so it's not surprising docs don't really make the distinction. And what do you call "type annotated &muts" ? – Levans May 30 '15 at 11:00
  • let b: &mut i32; b=a; Where else would I have to expect a non-move (e.g. a copy of some type) if it suits the compiler? Doesn't make me feel confident. But I'll accept your answer if that's the way it is. – dacker May 30 '15 at 11:02
  • The only non-reference types that are not moved are the ones implementing `Copy`, `&` references are `Copy`, and moving or re-borrowing a `&mut` reference are almost the same thing (and I don't see any situation where a move would be preferred over a re-borrow). That's basically all. Still, I'll open an issue in the compiler's Github to suggest they clarify it in the docs. – Levans May 30 '15 at 11:16
  • I see your point. My point is simply that the docs state something different than what's actually happening which, luckily in this case, does no harm. I can't think of an example that depends on a moved value actually being gone, either. Thx for the effort! – dacker May 30 '15 at 11:17
3

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.

Jmb
  • 18,893
  • 2
  • 28
  • 55