Questions about `&mut T` and move semantics, `&mut T` is "move-only"?

Your three-point model is very close to the truth, but falls ever so slightly short. I'm going to explain why, but first let me say that there isn't a compelling reason to alter your mental model. You might, like me, find the following interesting, but if your goal is just to write high-quality Rust code, this is likely not a language rule you need to commit to memory.

Close, but not exactly.

Again, close, but not exactly. Implicit reborrowing is a distinct rule that cannot be derived from applying deref coercion, even if you consider the lifetime a part of the type.

Consider this function:

fn no_implicit_reborrow<'a, 'b>(a: &'a mut (), b: &'b mut ()) {
    fn same_type<T>(_a: T, _b: T) {}
    
    same_type(a, b);
    //let _x = a; // error[E0382]: use of moved value: `a`
    let _y = b;  // OK
}

The caller of no_implicit_reborrow is what gets to choose 'a and 'b, so the function itself cannot suppose that 'a: 'b or vice versa. Nevertheless, same_type(a, b) compiles, because no_implicit_reborrow can choose T = &'c mut () where 'c is some lifetime in the intersection of 'a and 'b.

Although 'c is a different lifetime than 'a, a is not implicitly reborrowed; it is moved. You can tell this because uncommenting let _x = a; causes compilation to fail. However, b is not moved: it is implicitly reborrowed! This is because type analysis broadly goes left to right: once T is determined to be of the form &mut (), all other parameters of type T are subject to implicit reborrowing. All arguments are subject to lifetime subtyping, but only the first argument is moved.

On the other hand, consider this function:

fn implicit_reborrow<'a, 'b>(a: &'a mut (), b: &'b mut ()) {
    fn same_type<'c>(_a: &'c mut (), _b: &'c mut ()) {}

    same_type(a, b);
    let _x = a;  // OK
    let _y = b;  // OK
}

Again, the caller may choose 'a and 'b, but now same_type has two parameters of the form &'c mut (). The 'c that was notional in the last example is now explicit. The difference this makes is that now both arguments are subject to implicit reborrowing.

(You can also make the first example compile by telling the compiler that T is of the form &mut _ with a turbofish: same_type::<&mut _>(a, b).)

Inside the compiler, the way this works out is slightly different than what I said above. Notably, no lifetimes are actually resolved until after the implicit reborrow has been inserted. I'm sure it's possible to come up with an example that demonstrates that (by, for instance, introducing a borrowck error that wouldn't be detected until you fix the type error). In fact, lifetimes almost don't participate in type checking at all — the compiler initially assumes that all the lifetimes are compatible, and then borrowck comes through and checks those assumptions only after all the types have been resolved (including automatic steps like coercions and implicit reborrowing). However, I don't find arguments from compiler internals compelling; I just mention it to provide background for why the language is defined this way.

There's another difference between deref coercion and implicit reborrowing, which is that deref coercions are transitive (e.g. you can coerce from &mut Box<Box<Box<T>>> to &mut T transparently), but implicit reborrowing is not; however, since most places where reborrowing takes place are also coercion sites where deref coercion can also take place, it's not trivial to come up with an example that demonstrates this... I'm thinking one could adapt my first code example, but instead of working on that, I'm going to go to bed.

12 Likes