This borrow using &* works, but why?

Hello people,

I've been trying to understand how borrows work more deeply and I've run into a simple scenario that I can't make head nor tail of. It involves two snippets.

    let mut a = 1;
    let b = &mut a;
    let c = &*b;
    let d = *b;
    let mut a = 1;
    let b = &mut a;
    let c = &a;
    let d = *b;

The only difference is on the let c = ... line. The first one compiles and the second one doesn't. I think I understand the second one (mutable + immutable borrow is disallowed). My questions:

  • Why does the first one work; what difference does &* make?
  • Is this explained somewhere in the documentation?

Full disclosure: I came here after trying to read about stacked borrows and it's possible the answer is somewhere in there, but I'm very far from grokking all the rules.

1 Like

The difference is that c = &*b borrows via the lifetime borrowed by b -- then b will be inaccessible as long as c is "active" (only briefly thanks to NLL). Whereas c = &a tries to borrow directly from a, which it can't because b still has an active and exclusive &mut borrow for the later use in d.

4 Likes

Thanks!
So I guess I should read up more on NLL. Do you have a good reference for it? It doesn't seem to be covered much in the docs.

In the "Stacked Borrows" model (and elsewhere) this is called "reborrowing." For example, from the original blog post:

When we have an &mut i32 , we can reborrow it to obtain a new reference. That new reference is now the one that must be used for this location, but the old reference it was created from cannot be forgotten: At some point, the reborrow will expire and the old reference will be “active” again.

I also discussed re-borrowing briefly in Rust: A Unique Perspective.

2 Likes

NLL (Non-Lexical Lifetimes) just means determining how long lifetimes should be based on when things are used, rather than the lexical scope in which they're declared (in contrast to the earlier version of the borrow checker that did use lexical scope to determine lifetime). You don't really need to know the technical details behind it, but an explanation can be found here if you're interested.

The main idea of NLL (that a reference's scope depends on where it is used, not necessarily lexical scope) is mentioned briefly at the end of this section in the book, where it says:

Note that a reference’s scope starts from where it is introduced and continues through the last time that reference is used. For instance, this code will compile because the last usage of the immutable references occurs before the mutable reference is introduced:

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

The point @cuviper is making is that, unless you actually do something with c, it will be dropped just after being created, releasing the borrow of b.

3 Likes
let mut a = 1;
let b = &mut a;
let c = &mut *b; // changed to `&mut` for simplicity

leads to:

image

  • black arrows represent the relationship between an address and the memory it points to ("C pointers");

  • blue arrows represent the Stacked Borrows "authorization" / validity of such pointers:

    1. b is a valid pointer because it directly borrows the memory it points to (and it does so in an exclusive / unique manner, that's what the mut in &mut means);

    2. Then c is valid in and on itself thanks to it (also exclusively) reborrowing a through the borrow b; it is as if c had borrowed b's "license to access a": as long as c is valid, b cannot access a.

In other words, as soon as b accesses a, it means that c must have (maybe implicitly) forfeited its "license to access a", so as to have given it back to b. At that point, c becomes an invalidated / stale pointer: even though c may still live in memory, and even though its value is a memory address that points to b, Rust semantics forbid that such address be used to access a.

And this is what happens when you do:

let d = *b;

image

  • That's why, for instance, doing afterwards let e = *c; leads to a compilation error.

  • In your example however, c was not an exlusive (&mut) borrow but a shared (&) borrow; the situation is a bit more complex because b and c can still share access to a, (e.g., both can read it). That's why this playground does compile. It's just that while c is valid, b no longer has "exclusive access privileges" that would allow it to mutate a, so if b ever does mutate a, then the same reasoning as before applies and it means that c must have gotten invalidated beforehand.

    That's why the following code also fails to compile:

    let mut a = 1;
    let b = &mut a;
    let c = &*b;
    *b = 42; // use `&mut`-ness of `b`
    let e = *c;
    

Now, regarding:

let mut a = 1;
let b = &mut a;
let c = &a;

we have:

image

  • Indeed, c is directly borrowing from a even though b was exclusively borrowing a. This means that b must have implicitly forfeited its borrow and gotten invalidated.

This means that the following line let d = *b; is actually using an invalidated borrow, hence the compilation error.

  • But if it had been let b = &a;, then the borrow would have been shared and c would have been allowed to also get shared access to a without invalidating the b borrow (Playground).
12 Likes

Amazing! Thank you so much for this detailed answer, especially the pictures :heart:

Also thanks to everyone else for the explanations and references, they are (and will be) very helpful for shaping my mental models.

1 Like