This example subverts my understanding of ownership and reference scope. I'm confused

// print:
//  string-a
//  string-aaabc
fn main() {
    let mut a = String::from("string-");
    
    let s = &mut a;
    
    let ss = &mut a; // s goes out of scope here
    ss.push('a');
    println!("{}", ss.clone()); // ss.clone() ==> borrow shared reference, ss(mutable reference) should be gone out of scope. but it still available. why ???
    ss.push_str("aa"); // string-a
    
    let b = &mut (*ss); // borrow '&mut (*ss)' twice to mutable, ss shoudle be gone out of scope. but it still available. why ???
    // uncommenting,error(cannot borrow `*ss` as mutable more than once at a time)
    //      let b = &mut (*ss); ==> first mutable borrow occurs here  ==> why?
    //      ss.push('d'); ==> second mutable borrow occurs here  ==> why?
    //      b.push('b'); ==> first borrow later used here
    // why ? 
    // ss.push('d');
    
    b.push('b');
    ss.push('c'); // why? ss still available!

    println!("{}", a); // string-aaabc
}

Also posted on Rust:This example subverts my understanding of ownership and reference scope. I'm confused - Stack Overflow

1 Like

It's still in scope, but no longer usable.

Clone reborrows from ss. After clone returns, ss is no longer reborrowed.

This ends B's reborrow.

1 Like

I'm not sure what exactly it is you are asking.

If you are expecting lifetimes to exactly coincide with scopes, then: no, they are not identical. Lifetimes are much more fine-grained, and the compiler can decide that a lifetime ends well before the innermost scope ends.

4 Likes

yes! I also submitted on stackoverflow

The rule is that you can have either one &mut reference or multiple & references active at the same time. The Rust compiler considers a reference to be active while it is in use and finishes the borrow after its last use, so it effectively means that you can't interleave uses of references that would violate the rule.

Now, looking at your code, s is not used after ss is created, so that borrow is no longer active. You then call a series of methods on the ss reference. The clone() call is one you seem particularly confused about, and what that is doing is reborrowing ss as a & reference. That borrows through the existing &mut reference, rather than directly from a, so it isn't violating the rule. The clone() doesn't hang on to it though, it creates a new owned value, which you then drop, so there isn't much to analyse there. Note that the calls to push() and push_str() are also reborrowing

You then reborrow ss using &mut to create b. That reborrow is through ss so it restricts use of ss while b is active. Doing ss.push('d'); is then an error because it tries to reborrow ss again while b is still active (b is later used for a push just after it). Once the b reborrow has stopped being used, ss.push('c'); is fine, because b is no longer active.

4 Likes

The key mechanism at play is reborrows, which are underdocumented in the official sources.

    // This isn't used and its lifetime ends immediately  
    let s = &mut a;

    // Creating this `&mut` requires exclusive access to `a`,
    // and would conflict with `s` being used after here.
    // It's not used after here, so there is no conflict.
    let ss = &mut a;

    // Like the link says, this must be a reborrow of `*ss`
    // as `&mut` is not `Copy`.
    // The lifetime ends immediately after call.
    ss.push('a');

    // This is a shared reborrow but otherwise acts the same
    println!("{}", ss.clone());

    // A reborrow like with `push` above
    ss.push_str("aa"); // string-a
    
    // This is an explicit reborrow of `*ss`. Together the two
    // lines act the same as `ss.push('b')`
    let b = &mut (*ss);
    b.push('b');

All the uses of the underlying String data happened through the exclusive borrow ss (after it was created), so there's nothing that invalidates ss and you can still use it after everything above.

Let's compare to the non-compiling snippet:

    let ss = &mut a; // 'ss -------------+ // L0
    //                                   | // L1
    let b = &mut (*ss); // 'b -----+     | // L2
    //                             |     | // L3
    ss.push('d'); // -------------💥-----+ // L4
    //                             |       // L5
    b.push('b'); // ---------------+       // L6

b is a reborrow of *ss. For b.push to be possible on L6, its lifetime (borrow) must be alive on L3..=L6. But the reborrow of *ss on L4 is a use of *ss that conflicts with any other borrows of *ss being alive.

It's like how in my first code block, creating ss = &mut a makes s /* = &mut a */ unusable. Creating &mut a conflicts with other borrows of a being active; creating &mut *ss conflicts with other borrows of *ss being active.

So exclusive reborrows can be nested between uses of the original borrow, but they cannot "cross" with uses of the original borrow.

You can read some longer conversation and see some more examples by following the links in this post.

7 Likes

I appreciate that your page on reborrows says "tragically underdocumented". :frowning: I've been using Rust off and on for 2 or 3 years and I didn't know that mut refs don't implement Copy until I read this just now. It explains a lot. The compiler has been giving me errors all along that hinted at it, but for some reason I didn't get the implication, probably because it isn't stated explicitly in The Book or in other things I've read. Thanks!

But of course they… can't! One of the core tenets of the language is prohibiting shared mutability. If mutable references could be duplicated, that would literally be catastrophic, it would undermine the very memory model.

1 Like

Of course, I do understand that. But just because it has to work that way doesn't mean the full mechanism is documented in a way that everyone can comprehend quickly.

It is really the fact that reborrowing is done implicitly in many cases that must have prevented me from have a clear understanding of some of the errors I have seen with mut refs. Just like with lifetime elision, in the doc it helps to have the base behavior (without elision, or reborrowing in this case) explained first, and then add to that the sugar/conveniences that are used. The book does do that for lifetime elision, but not for reborrowing, and in fact reborrowing is not mentioned at all.

I think it is important to recognize the way that misunderstandings can occur, whether or not they are obvious to you, and I hope you will agree.

2 Likes

I hope someday someone will figure out the key to documenting these concepts in a way that people can quickly understand. Introductory Calculus instructors are still trying to do that.

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.