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

Given:

and

I'm inclined to believe that it's just the implicit reborrow that is needed to get Example 4 to work. y is already a mutable reference of the matching type.

// Example 4 - modified with explicit reborrow
fn f(z: &mut i32) {
    *z += 1;
    println!("{}", *z);
}

fn main() {
    let mut x: i32 = 5;
    let y = &mut x;
    
    f(y);               // implicit reborrow
    f(&mut *y);         // explicit reborrow

    *y += 1;
    println!("{}", *y); // '8'
    x += 1;
    println!("{}", x);  // '9'
}
1 Like

Correct. An example of where coercions come into play would be if the function took &mut [i32], and you had a &mut Vec<i32>:

fn f(zs: &mut [i32]) {
    for z in zs {
        *z += 1;
        println!("{}", *z);
    }
}

fn main() {
    let mut x: Vec<i32> = vec![5, 6];
    let y = &mut x;
    
    f(y);               // implicit reborrow and coercion
    f(&mut *y);         // explicit reborrow, implicit coercion
    f(y.as_mut_slice()); // explicit reborrow and conversion

    y[0] += 1;
    println!("{}", y[0]); // '8'
    x[0] += 1;
    println!("{}", x[0]);  // '9'
}
2 Likes

I agree with you on that. What you are saying is that, once an explicit "&mut..." occurs in the type, implicit reborrow happens which behaves exactly like deref coercion + stacked reborrow. We can definitely think of this as a whole syntax rule or a grammar sugar. But another way to think about it is as follows:

  1. &mut i32 itself is not a complete type. An lifetime annotation is needed to complete a reference type. (Either explicit by 'a notation or implicit by the compiler.)
  2. Assignment to a ref type from a different ref type (including lifetime difference, i.e., considering "complete" types only) results in (implicit) deref coercion, which behaves as if you wrote &mut *... or any other more complicated coercion results such as &mut *****.....
  3. Stacked-reborrows of &mut T will not invalidate the original mut reference. It will only push it to a "lifetime borrow-stack" so it's temporarily disabled. Once it's back on the (lifetime) stack top, it's revived.

These three rules by themself exists, are independent (and beautiful as well, especially the third one). On top of them, "implicit-reborrow" is another rule that can be deducted. It's definitely OK to make "implicit-reborrow" as a rule by itself but that seems not necessary.

Also consider the following examples:

fn f1(x: &mut i32) {
    let y: &mut i32 = x;

    x; // Compiles.
}

fn f2<'a>(x: &'a mut i32) {
    let y: &'a mut i32 = x;

    x; // Does not compile. Cannot move out of `x` because it is borrowed.
}
3 Likes

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.

10 Likes

Many thanks for the detailed illustration! Your examples are rock-solid!

While the lifetime is an aspect of the type not all aspects of a type are resolved at the same time. If I understand @trentj's post correctly deref coercion has already taken place by the time lifetimes are even considered.

Also consider the following examples:

fn f2<'a>(x: &'a mut i32) {
    let y: &'a mut i32 = x;

    x; // Does not compile. Cannot move out of `x` because it is borrowed.
}

I can easily rationalize that particular compilation error because stacked borrows cannot accomodate a situation where the lifetime of x and y have to be equal - the lifetime of y has to be strictly less than the lifetime of x.


To recap:

// Example 4a - the original puzzle
fn f(z: &mut i32) {
    *z += 1;
    println!("{}", *z);
}

fn main() {
    let mut x = 5i32; // `x` binds to a value of type i32
    let y = &mut x;   // `y` binds to a value of type &i32 with exclusive access to `x`
    f(y);

    // Rigorously applying move semantics only, the `y` binding should now be "uninitialized".
    // The `&i32` value with exclusive access (i.e. `&mut i32`) has been
    // moved into `f` - never to be seen again.
    // However the next line of code successfully uses the `y` binding.
    // If the value had been moved there should be a compilation error.
    //
    // Also as Example 3 showed a `&mut i32` value isn't Copy
    // (i.e. has move semantics rather than copy semantics)
    // even though the `i32` value is Copy (i.e. has copy semantics)

    *y += 1;
    println!("{}", x); // '7'
}

As @trentj outlined your confusion wasn't entirely unjustified - using generics:

// Example 4b - using parameterized types
use std::fmt::Display;
use std::marker::Sized;
use std::ops::{AddAssign, DerefMut};

fn f<T>(mut z: T, v: T::Target) where
    T: DerefMut,
    T::Target: Display + Sized + AddAssign {
    *z += v;
    println!("{}", *z);
}

fn main() {
    let mut x = 5i32; // `x` binds to a value of type i32
    let y = &mut x;   // `y` binds to a value of type &i32 with exclusive access to `x`
    f(y, 1);

    // *y += 1;
    // ^ uncomment the above and get the following compiler error:
    // move occurs because `y` has type `&mut i32`, which does not implement the `Copy` trait
    println!("{}", x); // '6'
}

The actual problem - people coming other programming languages expect Example 4a to work because that's the way references tend to work in traditional languages. In the absence of implicit borrows we would have to write:

// Example 4c - in a world without implicit borrows
fn f(z: &mut i32) {
    *z += 1;
    println!("{}", *z);
}

fn main() {
    let mut x = 5i32; // `x` binds to a value of type i32
    let y = &mut x;   // `y` binds to a value of type &i32 with exclusive access to `x`
    f(&mut *y);
    // i.e. forcing a reborrow to make things work

    *y += 1;
    println!("{}", x); // '7'
}

At this point many developers coming from traditional languages would call f(&mut *y); unergonomic, even though it's entirely consistent with move semantics.

So the implicit borrow present in Example 4a is a concession to developer ergonomics - at least for functions that don't use parameterized types (there the rules are a bit different).


Aside: I think going forward I'm going to classify features like implicit borrows and deref coercion as "you know what I mean programming support".

For example, I would have viewed manual deref in the presence of type inference as a kind of programmer's dead man's switch, a checkpoint system where the compiler could zap me awake because obviously I've stopped paying attention to the details (Bugs Abhor Loneliness).

1 Like

Yes. Now this makes more sense. I can see my three-point reasoning is flawed. @trentj also posted other interesting examples in #18 to help understand the process.

A follow up question:

What can happen if we let &mut T to be copyable? Previously I think of it as move-only because of the exclusive access. Now it turns out that lifetime check is an independent process after type checks. Isn't it better to let &mut T to be copyable to avoid the newbie issues when generic code kicks in (e.g. the previous stackoverflow example. Lifetime checks (including stacked-reborrows) happens anyway. From this point of view, forcing &mut T to be move-only seems unnecessary? Besides if we allow it to be copyable. Compiler could give better error messages about lifetimes instead of some Copy trait impl error. What do you think?

I doesn't really make sense for it to be Copy. You can't copy it — that's what it means for it to be exclusive.

I mean let's try to change out mindset and think a little bit more. Consider the following two examples:

// example 1
...
let mut x = 5;
let y = &mut x;

// y is moved out and inaccessible from now on
let z = y;
...
// example 2
...
let mut x = 5;
let y = &mut x;

// implicit re-borrow, as long as stacked-reborrow is obeyed, it's OK to continue using y.
let z: &mut i32 = y;
...

Example 2 works since & is explicit, which forces a re-borrow (I would say it's intrinsically a copy, of the reference). But why do we make the difference? If we make &mut T copyable and let borrow check/lifetime rules to ensure exclusive access. Wouldn't it be better? In that case, example 1 will be the same as example 2, and a bunch of generic code can work with &mut T with better ergonomics (which already exists in implicit-reborrow cases).

No, a reborrow is not a copy. A copy does not borrow from the value it was copied from — that's like their entire thing. It also causes all sorts of trouble with people suddenly being able to derive Clone or Copy on things containing a mutable reference.

1 Like

Note that reborrows have a shorter lifetime than the reference they are derived from, while a copy would have the same lifetime.

2 Likes

I'm a little bit confused. Let me ask a question to clarify: is the "copy" of const reference a borrow, or a true-copy? e.g.:

let x: i32 = 5;
let y = &x;
let z = y; // Is this a copy or a re-borrow?

My understanding is that, reborrow and copy/move are two different things. Reborrow is a semantics check for borrow/lifetimes. And copy/move are for type checks. They are two different process when parsing the syntax. Right? So why cannot we "copy" a mut reference and then check the borrow rules? For the Clone/Copy traits for structs containing mutable references. My gut feeling is that it will be fine if stacked-reborrow is enforced. E.g.:

struct MutRefWrapper<'a>(&'a mut i32);

fn main() {
    let mut x = 0;
    let y = MutRefWrapper(&mut x);

    // let z = y; // This will invalidate y as it's a move.

    let z = MutRefWrapper(&mut *y.0); // Workaround. This won't invalid y as it's an implicit reborrow.

    y; // Accessible.
}

If we allow Copy impl'ed for MutRefWrapper and enforce stacked-reborrow rule. It seems that let z = y would be a copy-OK statement?

Quick question: is "copy" of a const reference a copy or a reborrow? I'm a bit confused and I didn't find "reborrow" explained in the rust book. I don't think I understand the term clearly enough.

fn f(x: &i32) {
    let y = x; // Is this a copy?
    let z: &i32 = x; // is this a reborrow?
}

Going by the reference for a reborrow you are looking at ( &* or &mut * ) (implicit or explicit).

1 Like

Got it. Thanks for the pointer!

Shared references (&T) are copied. Unique references (&mut T) are reborrowed

To read more about this terminology and it's implications: https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html

5 Likes

Fundamentally the difference between a reborrow and a copy is that a reborrow is a borrow, and a copy is not.

When you reborrow a &mut T, that reference is mutably borrowed for the length of the reborrow. This means you can't use b in any way while the reborrow exists. This will fail to compile:

fn use_mutably(_: &mut &mut i32) {}

fn main() {
    let mut a = 10;

    let mut b = &mut a;
    let c = &mut *b;
    
    use_mutably(&mut b); // error!
        
    println!("{}", c);
}

playground

Had c been a clone of b, it would not have left b mutably borrowed. Sure, it would still be a mutable borrow of a, but it's important that it also mutably borrows b — otherwise we have two mutable borrows of the same variable a.

Consider the similar code:

fn use_mutably(_: &mut &i32) {}

fn main() {
    let a = 10;

    let mut b = &a;
    let c = &*b;
    
    use_mutably(&mut b); // ok!
        
    println!("{}", c);
}

playground

This compiles! Even explicitly trying to reborrow an immutable reference turns out to copy it. Note that had c been an immutable borrow of b, the above would not compile (playground) because then you would be trying to both mutably and immutably borrow b, which is not allowed.

So mutable references reborrow, and immutable references are copied.

Sorry I'm still confused and wasn't able to follow the intention of your examples. Let me ask in another way:
If mutable reference can be "reborrowed", why enabling the move anyways? Are there cases that a mutable reference has to be "moved" (so that the original one is invalidated) than "reborrowed"? It looks to me that this (stacked)reborrow model is a perfect modeling for mut reference. A move does nothing better other than invalidates the original variable (when it can be unnecessary). But the existence of move (for mut ref) seems to imply that "reborrow" is not everything. So what does it miss?

In the word of examples:

let mut x = 0;
let y = &mut x;
let z: &mut i32 = y; // reborrow
let w: &mut _ = y; // reborrow
let u = &mut *y; // reborrow
let v = y; // move. Why this has to be a move? Compared to a reborrow, it seems nothing better.

// What would happen if we enforce "reborrow" syntax for `&mut T` types (no move for `&mut`)?

Btw, thanks for your replies! You have been giving a lot of helps in my questions (not only this post)! I learned a lot. Salute!

Anyways I think I learned more than I could have imagined on this thread! Digging further seems to have diminishing returns. For now I'll just remember that let y = x is a move (when x is a mut ref), and to enable stacked-reborrow, one needs to use an explicit &mut in the declaration (or implicitly in function call site). That is good enough for almost all the use cases!