Rust mutable and borrow references

Hi everyone,
Im completely new to Rust and trying to get my head around references and the ownership/borrowing model.

i’m doing an small example that in one way works but not in the other, could you explain me why ?

This code does not work:
“Can not borrow expr_chars as mutable more than once at a time”

    let expr = String::from("2+1+3");
    let mut expr_chars = expr.chars().peekable();
    
    let fchar = expr_chars.next();
    let pfchar = expr_chars.peek();
    let schar = expr_chars.next();
    let pschar = expr_chars.peek();
    
    println!("{:?}", fchar);
    println!("{:?}", pfchar);
    println!("{:?}", schar);
    println!("{:?}", pschar);

but this one does

    let expr = String::from("2+1+3");
    let mut expr_chars = expr.chars().peekable();

    println!("{:?}", expr_chars.next());
    println!("{:?}", expr_chars.peek());
    println!("{:?}", expr_chars.next());
    println!("{:?}", expr_chars.peek());

as expected it prints 2 , +, +, 1

In rust the borrow ends when the borrowed value goes out of scope (or gets manually mem::drop()ed. When you do:

let fchar = expr_chars.next();

fchar still exists, so the mutable borrow is still tracked (The compiler is likely pointing you at the line that comes right after let pfchar = ...).

Your second example mutably borrows expr_chars, but always immediately releases it:

println!("{:?}", expr_chars.next());

There's no variable holding the result, so the borrowing is over as soon as the statement is done.

1 Like

First, you need to know that String is not an array of chars. It's an array of bytes in a format that is different than char. String's bytes are converted on the fly to a char in a small buffer. So when you peek into the iterator, you're not peeking into the string, you're peeking into a temporary buffer.

By the design of the Iterator interface, a call to .next() has a right to destroy the value that .peek() saw. Rust prevents seeing values that may have been destroyed.

When you keep all the variables around, then the calls to next invalidate your references saved from peek calls. If this code compiled, then you would see both fchar and schar have the same value! (because next replaces the one char in the shared location that you peek into).

If you print immediately, then you don't keep a reference to the maybe-destroyed value around, so there's no problem.

If you add .cloned() to peek then the problem goes away, because it makes it give you your own copy of the char, instead of a temporary permission to view temporary copy of single a char held inside expr_chars.

   
    let fchar = expr_chars.next();
    let pfchar = expr_chars.peek().cloned();
    let schar = expr_chars.next();
    let pschar = expr_chars.peek().cloned();
    
1 Like

Thank you @errado my mind is a bit more clear now, what I understood if i’m correct, expr.peek() returns a mutable reference hence locking the next calls to next().
something like this worked

let fchar = expr_chars.next();
{
   let pfchar = expr_chars.peek();
}. // pfchar dies here, 

let schar = expr_chars.next();
…
…

or

let fchar = expr_chars.next();
println!("{:?}", fchar);
let pfchar = expr_chars.peek();
println!("{:?}", pfchar);
let schar = expr_chars.next();
println!("{:?}", schar);

I guess this second one works because the calls to peek are dropped by print ?

Thank you @kornel, that’s a good point, interesting the thing about the “temporary” buffer,

if I want to keep a references (.peek) of something that will be changed later with .next that reference will not be valid anymore.

If we look at next, we can see that it needs to exclusively (mutably) borrow the iterator, but then it gives you back an Item with no connections to your original borrow. While if we look at peek, we see:

pub fn peek(&mut self) -> Option<&<I as Iterator>::Item>
//          ^ reference in       ^ reference out

Which further desugars into

pub fn peek<'borrow>(&'borrow mut self) -> Option<&'borrow <I as Iterator>::Item>

Which means that the borrow of the iterator extends into the returned item. Anywhere you use the returned &'borrow, your exclusive borrow of the iterator needs to still make sense.

But between your peek calls and using pfchar, you borrow the iterator again, and this invalidates your pfchar borrow for any further use. (For all the compiler knows, your call to next() freed the memory that pfchar was pointing to, for example.)

In graphical form:

    let expr = String::from("2+1+3");
    
    let mut expr_chars = expr.chars().peekable();
    let fchar  = expr_chars.next(); // -----------------------
    let pfchar = expr_chars.peek(); // ------,
                                    //       |
    let schar  = expr_chars.next(); // ------💥---------------- 
                                    //       |
    let pschar = expr_chars.peek(); // ---,  |
                                    //    |  |
    println!("{:?}", fchar);        //    |  |
    println!("{:?}", pfchar);       // <--💥-'
    println!("{:?}", schar);        //    |
    println!("{:?}", pschar);       // <--'

The exclusive borrows can't cross. It looks like we could preserve behavior here by moving the printing of pfchar (and thus fchar) up a bit, to get rid of the crossing:

    let expr = String::from("2+1+3");
    
    let mut expr_chars = expr.chars().peekable();
    let fchar  = expr_chars.next(); // -----------------------
    let pfchar = expr_chars.peek(); // ------,
                                    //       |
    println!("{:?}", fchar);        //       |
    println!("{:?}", pfchar);       // <----'

    let schar  = expr_chars.next(); // ----------------------- 
                                    //     
    let pschar = expr_chars.peek(); // ---,
                                    //    |
    println!("{:?}", schar);        //    |
    println!("{:?}", pschar);       // <--'

And indeed, this works.

1 Like

You got that right. The key is in holding a mutable reference to something and then trying to mutate that thing. @quinedot 's answer elaborates on this extremely well.

As for the second example:

Not quite. Notice that if you try to use pfchar again at the end you'll get the
same error: Rust Playground

The reason this example worked is because the compiler is smart enough to figure out that you aren't using pfchar anymore so it knows nothing funky can happen.

1 Like

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.