&&mut cannot be used to mutate value

Why I cannot dereference &&mut to mutate underlying value?

    let mut x = 5;
    let y = &mut x;
    let z = &y; // &&mut reference
    **z = 10;

compile error,

error[E0594]: cannot assign to `**z` which is behind a `&` reference
  --> src/main.rs:15:5
   |
15 |     **z = 10;
   |     ^^^^^^^^ cannot assign

error: aborting due to previous error

I don't think E0594 explains it well.

I tried with C++, the equivalent code is,

#include <iostream>

using namespace std;

int main() {
  int x = 10;
  int* const y = &x;
  typedef int* const y_ptr;
  const y_ptr* const z = &y;
  cout << **z << endl;
  **z = 11;
  cout << **z << endl;
  return 0;
}

and the output is

10
11

edited at 03/01/2020 9:30 pm

It, &&mut, is also mentioned in reasoning behind match ergonomics,

This is because an &mut inside of a & is still a shared reference, and thus cannot be used to mutate the underlying value.

To perform *... = ... mutation one needs the * to be applied onto an exclusive ( &mut) reference.

What & (&mut _) expresses is: a shared reference to a unique / exclusive reference to your value.

This results in / transitively resolves into a reference that is no longer exclusive, i.e., no longer &mut:

*(&&mut _) = &_

For more info, see:

3 Likes

the visuliaztion for above code,

y -> x
z -> y -> x

if I don't use pointer y to mutate the value? In other words, the memory bound to x is exclusively owned by y and shared by z. And I don't use y, does this mean z can exclusively access the memory of x? So I assume I can use z to mutate value.

Forget about the pointer, it's reference. In Rust &T is a shared reference of the value typed T, and &mut T is a unique reference of it. Normally you can only modify the value if you have unique access of it. This allows compile-time prevention of concurrency errors like iterator invalidation and data race. Some wrapper types, like RefCell<T> or Mutex<T> allows to modify inner value from its shared access, because the wrapper type can handle such concurrency issues correctly.

2 Likes

That line is telling Rust that z is a shared reference to y, and thus, transitively, a shared reference to whatever y refers to.

This is the same thing as with:

let mut x = 42;
let z = &x;
*z += 27;

The reason behind that not compiling, even though, in practice, z is the only pointer to x, is that, by design, you have declared that z is shared.

The trick in Rust is that "sharing" / aliasing is moved from being a runtime property to be a compile-time type-checked property. This leads to these examples where the mutation "could be legal", but that Rust deliberately forbids because they contradict the semantics of the code / of the types involved.

The idea is that if your reference is truly exclusive, then you should be able to change your code to be using &muts (e.g., define let z = &mut ...). And when you cannot, it probably means that the fact that the pointee is not aliased may not be that trivial to prove, so it may not even be true, so better be safe and use a construct that does not assume / require exclusivity (called Interior Mutability, c.f., the three links of my previous post) by:

  1. Having a runtime mechanism that guarantees exclusive access during, at least, some critical section (e.g., a Mutex),

  2. forbidding multi-threading and reentrancy, such as with the Cell type that by not being Sync prevents the former, and by never lending references, prevents the former.

    • Here is an example of using Cell:

      use ::core::cell::Cell;
      
      let x = Cell::new(5);
      let y = &x; // no longer needs mut, not even here
      let z = &y; // &&Cell reference
      (**z).set(10);
      

      which is one way of writing C's:

      int x = 5;
      int * y = &x;
      int * * z = &y;
      **z = 10;
      
4 Likes

Yeah, I understand the direct reference concept, but I'm confused with chained references, like && or &&mut. The book didn't mention that if we follow the reference chain and apply auto-dereference to find the non-reference value, whether we have the ownership on that value or not.

Under the hood, references are pointers with ownership checks. (I come from C++ background and I wish I could build some connection between rust and C++ to accelerate the learning).

Thanks for the answer!

Following the transitivity path to think about the "exclusivity" is really helpful and easy. I don't have to translate it to pointers and const pointers!

Mutable reference, including first-order or multi-order, has the exclusive access to the value. Immutable reference has shared access to the value.

&mut means unique, and & means shared. If the multi-order reference path has any &, then the pointee (pointed value) is shared, and it, the entire reference path, e.g., &&mut, should be considered as an immutable reference.

If the multi-order path only has &mut, e.g., &mut&mut, the pointer has unique access to the value, it should be able to be the mutable reference.

    let mut x = 5;
    let y = &mut x;
    let z = &y; // &&mut reference
    **z = 10;

For visualization, for example, dereference value pointed by path &&mut&,

& -> & -> &mut -> value, since the path has & that means this path is shared and thus locked to reject any mutation to the underlying value (think about shared reference is a read lock for DB table).

For &mut&&,

&mut ---------------> & -> & -> value
^^^^^^^^^^^            ^^^^^^^^^^
mutable so far        immutable afterwards

Cited from the book, the borrowing rule,

- At any given time, you can have either (but not both of) one mutable reference or any number of immutable references.
- References must always be valid.

In the beginning, I find it very difficult to think about multi-order references since the book only uses first-order & and &mut as examples to explain this rule. Adding on this transitivity path, I feel more comfortable thinking multi-order reference as a whole entity than thinking it as a compound type consisting of many references chained together.

3 Likes

It's more of a tree than a chain. Though the only way for the tree to branch is with shared references. This implies immutability, and to quote the nomicon:

  • Transmuting an & to &mut is UB
    • Transmuting an & to &mut is always UB
    • No you can't do it
    • No you're not special

That said, you can hold as many exclusive-references-to-exclusive-references as you need. So long as the tree of references is not mutated, lest ye invoke the wrath of the borrow checker.

    let mut x = 5;
    let mut y = &mut x; // &mut reference
    let z = &mut y; // &mut &mut reference
    **z = 10;

Note the binding for y must also be mutable.

playground

Try inserting *y = 5; just before the assignment to **z...

1 Like

Thanks, that's helpful!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.