9

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:

  • Variable y has type &mut i32, or in plain English "A mutable reference to an integer".
  • Variable z has type &&mut i32, or in plain English "An immutable reference to a mutable reference to an integer".
  • By dereferencing z once (i.e. *z) I will get something of type &mut i32, i.e. something of the same type as y. However, dereferencing this again (i.e. **z) gets me something of type i32, but I am not allowed to mutate that integer.

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
domin
  • 1,192
  • 1
  • 7
  • 28
  • *"help: consider changing this to be a mutable reference: `&mut y`"* – hellow May 06 '19 at 12:24
  • Yeah, that will make the error go away, but that's not the issue I am having. I want to understand the promises that types give. – domin May 06 '19 at 12:34
  • 1
    _"By dereferencing `z` once (i.e. `*z`) I will get something of type `&mut i32`"_ Nope, that would be a mutable deref, which cannot be done over an immutable reference. You can at best obtain a `&i32` from there. – E_net4 May 06 '19 at 13:31
  • @E_net4: That's interesting... So there are multiple types of derefs? So if I have a `&&mut i32` and perform an immutable deref on it, what type will I get? `&i32`? What would be the derivation rule of that? – domin May 06 '19 at 13:49
  • 1
    Those would be [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html) and [`DerefMut`](https://doc.rust-lang.org/std/ops/trait.DerefMut.html). The documentation of each trait explain which is used in which context. – E_net4 May 06 '19 at 13:52
  • Ah, good to know that! I am getting closer... However, I don't know how the compiler "implements" the trait for built-in references. So going back to my question, the signature is `fn deref(&self) -> &Self::Target`, how do I instantiate that with my `&&mut i32`? Is the target `i32` and therefore I get `&i32` back? How come that the target of a `&&mut i32` is `i32`? Isn't that skipping a level? – domin May 06 '19 at 14:03
  • That is answered by [this question](https://stackoverflow.com/q/31624743/1233251). – E_net4 May 06 '19 at 14:29
  • 2
    @domin No, the type `<&&mut i32 as Deref>::Target` is `&mut i32`, as you can see from the documentation linked above. Looking at the definitions of the `Deref` trait doesn't really help here, since the relevant step is built into the compiler, not the standard library. – Sven Marnach May 06 '19 at 14:31
  • My gosh this is a complicated mess for a beginner. ;) So, in this case, what exactly are the semantics of the built-in compiler-deref? What is `&&mut i32` transformed into after doing one compiler-built-in-deref? – domin May 06 '19 at 14:42
  • [What are Rust's exact auto-dereferencing rules?](https://stackoverflow.com/q/28519997/155423) – Shepmaster May 06 '19 at 15:18
  • 3
    Sometimes it helps to think of *exclusive* and *shared* references rather than *mutable* and *immutable* ones. So `y` is an exclusive reference to an integer, and `z` is a shared reference to an exclusive reference to an integer, but you can't dereference `z` to get `y`-like *exclusive* access to the integer, because that violates `z`'s own contract of *sharedness*. – trent May 06 '19 at 16:05
  • @trentcl Great answer, thank you! So that means I sort-of have to consider multiple deref-steps as a single operation, as clearly I cannot just structurally decompose a type like `&&mut i32` step-by-step. – domin May 06 '19 at 18:37
  • @domin You can consider multiple steps of dereferencing as separate operations. The type of `*z` is indeed `&mut i32`. The code `let foo: &mut i32 = *z;` will pass the type checker just fine, but the borrow checker will complain that you are creating a mutable borrow for something that's behind a shared reference. You don't need to understand how the borrow checker works in detail; it's enough to understand that it makes sure that nothing behind a shared reference can be mutated and that mutable references can't be aliased. – Sven Marnach May 07 '19 at 07:31
  • @SvenMarnach That's another peculiar behavior: In `let foo: &mut i32 = *z;` I would expect this to be equal to `let foo = *z;` as it just makes the type explicit. However, the latter triggers a different compiler error ("cannot move out of borrow"). So specifying the type here seems to actually do this: `let foo = &mut **z;`. In any case: I think the crucial insight is that the borrow checker does not derive its rules solely from the types, but it has additional information to work on. – domin May 07 '19 at 08:03
  • 1
    @domin As I said before, the borrow checker is a complex beast, but there simply isn't a need to understand how it works. You can trust it to maintain the invariants it is supposed to maintain. And good observation about the different errors – adding the type here explicitly indeed changes the semantics. Without the type, the let binding _moves_ the mutable reference, and with an explicit type annotation the binding creates an implicit reborrow. This only happens for mutable references and is a rather subtle difference. – Sven Marnach May 07 '19 at 08:15

1 Answers1

14

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.

Brian Campbell
  • 322,767
  • 57
  • 360
  • 340
  • Thanks a lot for your detailed answer! So multiple stacked references should still be read as references pointing – directly or indirectly – to the same underlying owned value (`T`)? If that's the case, then why can I take a `let foo = &mut &mut x;` (where `x : T`), and afterwards dereference that one just half-way-through and mutate the middle reference, like this: `*foo = &mut y;` where `y` is another value of type `T`? This operation would have nothing to do with the original `T`, so a reference must be an object (with an owner) in and by itself... – domin May 08 '19 at 06:29
  • 1
    Yes, a reference is an object with an owner itself. But the permission you get is based on the path you reach it. If you reach an `&mut` reference through an `&` reference, you only have shared, read-only access, to the reference itself and to anything it refers to. – Brian Campbell May 08 '19 at 13:24
  • 1
    I should probably clarify: multiple stacked references aren't read just as references pointing to the underlying value. They are pointing to what they are pointing to. But the permission you get is the minimum of the permissions conferred by each reference in the path you reach it. I will edit my answer to be more clear. – Brian Campbell May 08 '19 at 13:30
  • Awesome, thanks for the clarification. I've just played around a bit with that knowledge and came up with this sample: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c4d2880792180205d906fde034117238 Do you agree with my comments there? – domin May 08 '19 at 14:02
  • 1
    @domin Yes, that's right. The borrow checker works statically, and while it could be possible to statically analyze this particular piece of code, if you instead passed these into a function, it wouldn't be able to tell which reference `foo` referred to after the function returned (https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fe87f5ed958e0be75618e8e6d35feb30). So the compiler applies conservative rules; it determines that `foo` could reference either `x` or `y`, so as long as `foo` is alive, you can't make other references to `x` or `y`. – Brian Campbell May 08 '19 at 21:10
  • 1
    Right. A typical set-union-analysis then! Thanks a lot for your help! – domin May 08 '19 at 21:32