rustimmutabilityborrow-checkermutability

How to interpret immutable references to mutable types in Rust?


It seems that I cannot mutate anything if there is any immutable reference in my chain of dereferencing. A sample:

fn main() {
    let mut x = 42;
    let y: &mut i32 = &mut x; // first layer
    let z: &&mut i32 = &y; // second layer
    **z = 100; // Attempt to change `x`, gives compiler error.

    println!("Value is: {}", z);
}

I'm getting the compiler error:

error[E0594]: cannot assign to `**z` which is behind a `&` reference
 --> src/main.rs:5:5
  |
4 |     let z: &&mut i32 = &y; // second layer
  |                        -- help: consider changing this to be a mutable reference: `&mut y`
5 |     **z = 100; // Attempt to change `x`, gives compiler error.
  |     ^^^^^^^^^ `z` is a `&` reference, so the data it refers to cannot be written

In some way, this makes sense, as otherwise the compiler would not be able to prevent having multiple mutable access paths to the same variable.

However, when looking at the types, the semantics seem to be counter-intuitive:

In essence, the types of references in some sense lie to me, as they don't actually do what they claim they do. How should I read types of references properly in this case, or how else can I restore faith in that concept?

Testing with this sample:

fn main() {
    let mut x = 42;
    let y: &mut i32 = &mut x; // first layer
    let m: &&mut i32 = &y; // second layer
    let z: &&&mut i32 = &m; // third layer
    compiler_builtin_deref_first_layer(*z);
}

fn compiler_builtin_deref_first_layer(v: &&mut i32) {
    compiler_builtin_deref_second_layer(*v);
}

fn compiler_builtin_deref_second_layer(w: &mut i32) {
    println!("Value is: {}", w);
}

The parameter types of those last two functions are correct. If I change any of those, the compiler will complain about mismatched types. However, if I compile the example as-is, I get this error:

error[E0596]: cannot borrow `**v` as mutable, as it is behind a `&` reference

Somehow, the call to compiler_builtin_deref_first_layer seems to be okay, but the call to compiler_builtin_deref_second_layer isn't. The compiler error talks about **v, but I only see a *v.


Solution

  • In essence, the types of references in some sense lie to me, as they don't actually do what they claim they do. How should I read types of references properly in this case, or how else can I restore faith in that concept?

    The right way to read references in Rust is as permissions.

    Ownership of an object, when it's not borrowed, gives you permission to do whatever you want to the object; create it, destroy it, move it from one place to another. You are the owner, you can do what you want, you control the life of that object.

    A mutable reference borrows the object from the owner. While the mutable reference is alive, it grants exclusive access to the object. No one else can read, write, or do anything else to the object. A mutable reference could also be called an exclusive reference, or exclusive borrow. You have to return control of the object back to the original owner, but in the meantime, you get to do whatever you want with it.

    An immutable reference, or shared borrow, means you get to access it at the same time as others. Because of that, you can only read it, and no one can modify it, or there would be undefined results based on the exact order that the actions happened in.

    Both mutable (or exclusive) references and immutable (or shared) references can be made to owned objects, but that doesn't mean that you own the object when you're referring to it through the reference. What you can do with an object is constrained by what kind of reference you're reaching it through.

    So don't think of an &&mut T reference as "an immutable reference to a mutable reference to T", and then think "well, I can't mutate the outer reference, but I should be able to mutate the inner reference."

    Instead, think of it as "Someone owns a T. They've given out exclusive access, so right now there's someone who has the right to modify the T. But in the meantime, that person has given out shared access to the &mut T, which means they've promised to not mutate it for a period of time, and all of the users can use the shared reference to &mut T, including dereferencing to the underlying T but only for things which you can normally do with a shared reference, which means reading but not writing."

    The final thing to keep in mind is that the mutable or immutable part aren't actually the fundamental difference between the references. It's really the exclusive vs. shared part that are. In Rust, you can modify something through a shared reference, as long as there is some kind of inner protection mechanism that ensures that only one person does so at a time. There are multiple ways of doing that, such as Cell, RefCell, or Mutex.

    So what &T and &mut T provide isn't really immutable or mutable access, though they are named as such because that's the default level of access they provide at the language level in the absence of any library features. But what they really provide is shared or exclusive access, and then methods on data types can provide different functionality to callers depending on whether they take an owned value, an exclusive reference, or a shared reference.

    So think of references as permissions; and it's the reference that you reach something through that determines what you are allowed to do with it. And when you have ownership or an exclusive reference, giving out an exclusive or shared reference temporarily prevents you from mutably accessing the object while those borrowed references are still alive.