Why is there no error when displaying a variable that has been mutably referenced?

Hello,

The problem :

It seems that it is possible to display the value of a mutable variable having a mutable reference.

I say this because according to a certain video, using println! to display a variable, will create an immuatable reference to that same variable. However, we know that we can't have a mutable and an immuatable reference at the same time.

The video is this one (at 3min 18) : https://youtu.be/u4KyvRGKpuI?t=198

and in the video, the person gets an error.

The question :

I tried to display a mutable variable that had a mutable reference. I used println! and it worked, the mutable variable named country has been successfuly displayed, even as it was mutably referenced. So how would it be possible for pintln! to assign an immutable reference to an already mutably referenced variable without this generating an error during error control ?

for example :

fn main()->() {

    let empty = ""; //don't account for this variable as it is only there as a mean of better displayment when using println!
    
    
    let mut country = "Arabia";
    println!("{0: <10}{1:><30} original",country,empty);
    
    
    country = "Japan";
    println!("{0: <10}{1:><30} mod1",country,empty);
    
    println!("|");
    println!("|");
    println!("|");
    println!("|");
    
    
    let reference = &mut country;
    println!("{reference: <10}{empty:><30} reference to mod1");
    
    *reference = "Germany";
    
    println!("{country: <10}{empty:><30} reference to mod2");
    
}


If you make this program run, it will in effect display the variable country without errors.

This is strange because right before, on the line let reference = &mut country; we clearly see that the new variable reference is a mutable reference for the variable country.

Despite all this, the line println!("{country: <10}{empty:><30} reference to mod2"); sill works and displays the value Germany

Thanks and have a good day

This is an oversimplification. You cannot have a usable mutable reference at the same time as an immutable reference to the same data. The immutable reference was borrowed from the mutable reference, so the mutable reference is not usable for mutation as long as that is true.

Reborrowing from existing references is always possible, and follows the same rules as borrowing from owned data, restricting its use until the borrow is released.

4 Likes

Thank you for these useful informations, I would like to ask you about the aforementioned video, and the reason for the error that happened there.

In the video, if a reborrow happened, then why did an error happen during error control ? Or maybe I did something different without noticing.

Thanks for the help

The video is 7 years old! Rust's borrow checking was enhanced since then as described here:
https://smallcultfollowing.com/babysteps/blog/2016/04/27/non-lexical-lifetimes-introduction/
https://smallcultfollowing.com/babysteps/blog/2018/10/31/mir-based-borrowck-is-almost-here/

5 Likes

Thank you for this useful information as well as for the help, I'll see the link right now

These 2 webpages seem very important, they have a lot of important informations and it makes them extremly useful for people.

Thank you for sharing this website, I don't understand all the information yet, however I have a clearer view than before on error control as well as other aspects of the rust programming.

It's maybe as you said that the error in the video was because of the old time when it was made.

1 Like

The discussions under the youtube video mention multiple outdated parts of the video and they also mention NLL (the links I sent) as the change that made them outdated.

1 Like

So to summarize, pre-NLL borrows that were longer than a statement or so[1] always extended to the end of the lexical block:

// Pre-NLL example (used to fail, now compiles)
fn main() {
    let mut x = 10;   //        pre-NLL
    let dom = &mut x; // --- borrow scope ---+
    *dom += 1;              //               | (<-- post-NLL borrow ends here)
    println!("x is {}", x); //               |
} // ----------------------------------------+

But nowadays, lexical scopes and references going out of scope interact hardly at all.[2]

However, even back in the NLL days, "no other references if you have a &mut" was an oversimplification. For example, this program worked.

// Pre-NLL example
fn main() {
    let mut x = 10;
    let dom = &mut x;
    *dom += 1;

    { // limit the lifetime of `temp` (a pre-NLL workaround)
        let temp = &*dom;
        println!("x is {}", temp); 
    }

    *dom += 1;
}

And the language has to support this reborrowing pattern in order to be usable in a practical way. Keep in mind that &mut _ aren't Copy -- they can't be due to their exclusiveness guarantee -- but you often need to pass them elsewhere and then use them again.

// Also works pre-NLL
fn example(vec: &mut Vec<i32>) {
    // We pass a `&mut Vec<i32>` here, like `Vec::push(vec, 0);`
    vec.push(0);
    // But can still use `vec` afterwards
    println!("New contents: {:?}", vec);
}

The program works because *vec was automatically reborrowed in the call to push, so we didn't give away vec. After the call returns, the reborrow expires, and we can use vec again. (And the fact that this works pre-NLL demonstrates that not all borrows extended to the end of the lexical scope even then.)


I suggest you find some learning material that is at least post-NLL, as that was a very significant change. Unfortunately, even maintained and newly written learning material tends to try to teach borrows as if they were lexical or by analogy with scopes, which isn't really accurate. Just be aware that such material is trying to give you the general idea of borrowing, and not the actual borrow-checker analysis.

Finally, you'll eventually learn about things like RefCell and Mutex which allow shared mutation. I suggest thinking of &mut _ as an exclusive reference and thinking of & _ as a shared reference now. Those are more accurate terms than "mutable" and "immutable" (despite having mut in the type), and knowing that upfront may save you from having to adjust your mental model and relearn things later.


  1. I never learned pre-NLL lifetimes in enough depth to know the exact rules ↩︎

  2. You just can't have an active reference to the reference itself, like a other = &dom ↩︎

3 Likes

I should read the mentions next time, initialy I was reading the easy rust website by Dave Macleod, and I was stuck at the references part.

The link is there in case there are other people who'd want to see the website :
https://dhghomon.github.io/easy_rust

Yes, there isn't a lot of documentation on this aspect of the language, that I can find. However, "the book" at least mentions that borrows don't always last until the end of the block:

https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references

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, the println!, occurs before the mutable reference is introduced:

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // variables r1 and r2 will not be used after this point

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

The scopes of the immutable references r1 and r2 end after the println! where they are last used, which is before the mutable reference r3 is created. These scopes don’t overlap, so this code is allowed: the compiler can tell that the reference is no longer being used at a point before the end of the scope.

1 Like

Thank you very much for all this information, it is true that exclusive reference and shared reference are better terms. When it comes to something like let a = &* b wouldn't it make the & and the * cancel eachother ?

Here is a list of what I use to understand the rust programming :
-rustlings
-Learn Rust in a Month of Lunches (by Dave Macleod)
-Easy rust website (by the same person)
-rustup doc (in the cmd)

They are very useful and updated, however I'm a slow person. The 2 links added by jumpnbrownweasel are also leading to an interesting website (https://smallcultfollowing.com)

I add this list in case someone would use it

Thanks again and have a good day

This example that you sent reminded me of this exercise available in rustlings, it's called move_semantics4.rs

It's a nice exercise because it helps to avoid the kind of oversimplifications that I did earlier.

Thank you very much and have a good day

|
|
|

Just in order to clarify more, I'd like to use your example in order to add a 4th shared reference.


fn main() {

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{r3}");
    
    let r4 = &s;
    println!("{r4}") //this will print r4 very well
}

This would confirm all that even more. As the initial question was induced by a video talking about println! doing something similar to a shared reference, which wouldn't have worked after an exclusive reference. However that was during the older times many years ago.

Thanks again for the nice example.

Type-wise, yes, if you have a b: &T, then &*b will also be a &T.

When I had let temp = &*dom; I was going from a &mut T to a &T though, and those are different types. That said, it will still happen automatically if you do let temp: &_ = dom -- if a shared reference is expected but an exclusive reference is supplied.

But sometimes a &mut _ will be moved and not automatically reborrowed, in which case you may need to perform the reborrow "manually" with &mut *variable. Here's a recent example.

1 Like

Thank you very much for this detailed answer as well as the linked recent example.

Have a good day and thanks for all the answers