Why can I assign a mutable reference to an immutable reference without violating the borrowing rules?

The following code (it compiles!) first declares a mutable variable s which has the type String. Then it creates r1 which is a mutable reference to s. Later on, it assigns r1 to r2 which is an immutable reference to s by coercing r1 into type &String and it even assign r2 to r3 which is another immutable reference to s. Isn't the reference scope of r1 fully covers the reference scopes of r2 and r3 since there is a p1.push_str("ghi") statement at the end, so it violates the borrowing rules? To further prove my point I printed the address that r1, r2 and r3 had pointed to and they turns out to be the same address. How is this possible?

fn main() {
    let mut s = String::from("abc");

    let r1 = &mut s;
    r1.push_str("def");

    let r2 = r1 as &String;
    let r3 = r2;

    println!("{:p}", r1); // 0x16b916210
    println!("{:p}", r2); // 0x16b916210
    println!("{:p}", r3); // 0x16b916210

    r1.push_str("ghi");

    // uncomment the following two lines and the code
    // won't compile.
    // println!("{}", r2);
    // println!("{}", r3);
}
4 Likes

When you create r2 you "reborrow" r1, which limits what you can do with r1 while r2 (and anything derived from it, like r3) is alive. In particular since you created r2 as shared, r1 is disallowed to be used for anything that requires mutable access, but can still be used for immutable access, as if it was temporarily downgraded to a immutable reference.
However once you use r1 in a mutable way (for example by calling push_str) then r1 gets "reactivated" as a mutable reference, and the reborrows (r2 and r3) are invalidated and won't be able to be used later.

8 Likes

Thanks for your answer. It really makes sense. But is there a relevant official document about this stuff?Such as the "reborrow" and "reactivation" you mentioned? I'm fairly new to Rust, sorry for the touble.

Not now. But it's already what many people want better documentation of reborrowing · Issue #788 · rust-lang/reference · GitHub

I collect some links about reborrows: better documentation of reborrowing · Issue #788 · rust-lang/reference · GitHub

But I recommend you first read Copy and reborrows - Learning Rust (and go through all the chapters)

5 Likes

Thanks, I'll check them out. :blush:

The limitations that the compiler enforces are that mutable borrows must be exclusive and other usage of the borrowed thing cannot happen, even immutable borrows, while the mutable borrow exists. This is likely the reason why you are confused, as r1, r2, and r3 all point to the same string s, and co-exists, but r1 should be exclusive, right!?

One must however not confuse the abstract, somewhat syntactical, and compile-time-only notion of “borrowing” with the question “what does the pointer point to”. In fact only r1 borrows s. r2 however borrows r1. More precisely, it borrows the target of r1, often written *r1, but this is a minor detail, and a good first approximation that works for many cases is to consider a borrow of r1, so e.g. like a reference of type & &mut String being created, which could then be de-referenced into a &String reference as a second step.

How it comes about that r1 is borrowed is easy to explain: The coercion r1 as &String is implemented as, i.e. equivalent to writing, the expression &*r1, which – unsurprisingly – simply borrows *r1.

    let r2 = &*r1; // r2 borrows `*r1`
    let r3 = r2; // r3 is a copy of r2

The compiler will consider r1 a new kind of owner, in a sense: it owns mutable access to the String in question. The compile does not draw any connection between *r1 and the original string s, so the code is significantly different from writing &s instead of &*r1, as far as the borrow-checker is concerned.

With the basic idea that r2 and r3 borrow from r1, all that’s necessary to require the full code’s behavior is the knowledge that println!("{:p}", r1) also only immutably borrows r1, so this access to r1 does not conflict with the existence of r2 and r3.

The minor difference that *r1 is borrowed, not r1, has one effect in particular: If you borrow something, you usually cannot keep this borrow after the borrowed thing goes out of scope, but a re-borrow like r2 = &*r1 can exist for longer than the scope of the variable r1. This is because nothing of importance to this re-borrow is actually held in the variable r1 itself, and the compiler offers some special reasoning that works as if you imagined that the original borrow of s stored in r1 just lives on somewhere else, e.g. in your imagination, in the void, etc…


By the way, many cases of (implicit) re-borrows you are probably already familiar with, even though they might not strike you as all that remarkable in the first place. For example, looking at simple code like

struct S(…);
impl S {
    fn f(&mut self) {…}
    fn g(&self) {…}

    fn h(&mut self) {
        self.g();
        self.f();
    }
}

if you think about what happens in the implementation of h for long enough, you will realize that there is an immutable &S reference being passed to (and usable in) g, but all the time the mutable self reference in h still keeps existing, as is evident by the fact that f is still callable afterwards! This, too, exercises a re-borrow! The call desugars from self.g() to S::g(&*self). In fact, the call to f will also re-borrow, but mutably, desugaring to S::f(&mut *self). This is evident by the fact that the call self.f() does not move self, otherwise you couldn’t call it twice, as in:

    fn h1(&mut self) {
        self.g();
        self.f();
        self.f(); // works fine, too!
    }

So reborrows are, interestingly enough: on first look a seemingly special and seemingly niche feature and one can go learning and using Rust without knowing about reborrowing for a relatively long time; on second thought they are something that can (mostly) be actually easily understood, since if borrows (i.e. references) are also understood of some sort of owned values that can be borrowed yet-again, a reborrow is just a “borrow of a borrow”; and finally, it turns out reborrows happen implicitly everywhere in Rust all the time, and without them, the most straightforward code examples wouldn’t even compile anymore :smiling_face:

7 Likes

Thanks for your answer! However, this reborrow thing is still too ambiguous for me. :smiling_face_with_tear:

According to your answer (based on my understanding), r2: &String is actually an immutable borrow of r1: &mut String where Rust acheives this by borrowing *r1. But aren't *r1 and s the same thing since *r1 is simply dereference of type &String? And they even point to the same memory address! What's the difference if let r2 = &s? Don't both situations violate the aliasing rule of Rust?

Why they don't have any connections? They are actually the same thing from my perspective!? :cry:
I really wonder how Rust's borrow checker defines the "connection" between *r1 and s.

Feel free to also look into (especially the first part(s) of) this post of mine What is a good mental model of borrow checker? - #18 by steffahn, maybe it helps with further understanding, I don't know, but it already exists, so I can just link it ^^

The difference between s and *r1 is indeed (usually) non-existent at run-time, when borrowing either will create exactly the same pointer.[1] However, when borrowing *r1, you give the borrow checker the extra information that r1 should only be temporarily restricted (in much the same way in which s is restricted when it's borrowed, i.e. a mutable re-borrow of *r1 disallows any access to *r1 for the duration of that re-borrow, and an immutable re-borrow only disallows mutable access). On the other hand, directly borrowing s again will completely end every pre-existing borrow of s, so any later usage of r1 would be disallowed.

Regarding the question "why are these different", the first thing to note is that the re-borrowing case is less restrictive, so it's important that that operation works as intended when we want to use it, as temporarily restricting one borrow is a useful, and safe, operation, and it makes sense from the principle that types like & &mut String exist anyways! So we do definitely not want to turn any of today's cases of re-borrowing into error cases, when they can compile successfully just fine, while maintaining memory safety.

The reasonable follow-up question of “why not make borrowing s directly less restrictive in the same way then?” probably only has the answer that that's simply a lot harder to pull off (for the compiler): it would require more complexity in the borrow checker. There is no real strong, principled, reason I can think of why, especially a locally appearing in the same function, existing mutable borrow such as r1 in your example code couldn't implicitly be temporarily restricted by a new reference let r2 = &s; in generally the same way let r2 = &*r1; does, other than the reason “no one had proposed a full set of rules for such borrow-checking behavior yet”. Perhaps some people might argue though that it's also in a sense “less confusing”, especially for mutable re-borrows, when the target of a mutable reference like r1 can, while it exists, only ever be mutated by someone literally accessing “*r1”, and not indirectly by accessing the different variable s directly. On the other hand, there are cases where such behavior does seem rather desired, e. g. if you have a locally defined closure let f = || s.push_str("foo");, you might want to use such a closure for code-reuse purposes in use cases where you call pattern might look like f(); s.push_str("bar"); f() (expecting the result to be s == "foobarfoo") and more advanced borrow checking algorithm working along similar lines as the idea of making &s and &*r1 behave identically might be able to pull this off, too.


  1. There actually is a difference in the abstract “stacked borrows” model that the tool miri implements, in that borrowing s will result in the previously existing r1 to become entirely invalid, whereas borrowing from *r1 will not invalidate r1 but merely restrict its usability temporarily, until the borrow of *r1 ends. ↩︎

1 Like

I think it should work for direct references, but not for any borrow in general, which might carry another meaning. For example consider this code:

let mut cell = RefCell::new(foo);
let mut_ref = cell.get_mut();
let another_mut_ref = cell.borrow_mut();

mut_ref must not be usable after cell.borrow_mut(), not even as shared, because another_mut_ref has a mutable/exclusive reference to the contents of the RefCell.

The problem is that there's already the expectation that mutable borrows can not be downgraded, and there's code which soundness relies on this.

2 Likes

Indeed once any custom function gets involved, such a feature could not support those, unless it require a new kind of reference type or lifetime annotation.

The connection is implicit in the lifetime constraints and "loans" calculated as part of borrow checking. For example here:

    let r1 = &mut s;

Let's say &mut s has livetime 's, and r1 has type &'r1 mut String. Then there is an exclusive loan of s for 's, and a constraint that 's: 'r1.

And then here:

    let r2 = r1 as &String;

Let's say the cast on the right has lifetime 'as and r2 has type &'r2 String. Then there is a shared loan of *r1 for 'as, and the constraints 'r1: 'as, 'as: 'r2.

Altogether we have 's: 'r1: 'as: 'r2.

Now, let's say you edit the code to move s before you print r2. Printing r2 means the lifetime 'r2 has to still be active at the println!, and the lifetime constraints force 's to still be active too. The compiler will see the move of s and look for any related loans; it will find the loan of s with lifetime 's that is still active. The move is incompatible with the loan, and an error is reported.


In your code with r2, r3, and the println!s, you have a bunch of shared loans and reborrows, but none of the uses conflict with shared borrows. All of them can end before the push_str (as they aren't used in or after that).

If you uncomment the trailing println!s though, it forces the shared loan of *r1 to be active at the push_str (due to the lifetime constraints), and the use at the push_str is not compatible with having a loan of *r1 active. So as with the move example, you get a borrow checker error in that case.


More details are spelled out in the NLL RFC, but it's pretty involved.

1 Like

Thanks for your comprehensive response! I’m getting the hang of it. :smiling_face:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.