Typical can't borrow issue

#1

Hi Rust coders.

I thought if I put a mutable borrow inside a scope block that once the block exited the borrow would disappear too, but this doesn’t seem to be the case. Can someone explain why Rust rejects my borrow issue?

Playground example

error[E0502]: cannot borrow `w` as immutable because it is also borrowed as mutable
   --> src/main.rs:153:5
    |
149 |         let sm = w.get_sm_mut();
    |                  - mutable borrow occurs here
...
153 |     w.print();
    |     ^
    |     |
    |     immutable borrow occurs here
    |     mutable borrow later used here

Why doesn’t the scope block work? Am I missing understanding scopes perhaps?

Thanks.

0 Likes

#2

Don’t store references in structs, instead use a Box to store you traif objects and get rid of lifetimes.

0 Likes

#3

Thanks.

However, my issue isn’t with trait objects it is with borrowing within a scope. The checker can’t seem to tell that a borrow is restricted by a scope thus blocking any calls on my “world” object after the scope.

And I use Rc-RefCells instead of boxes.

0 Likes

#4

Yes, this is symptomatic of using storing references in structs. When you do that Rust pretends that the struct is also a reference, but without the special casing that normal references get.

Rc Would also work instead of Box.

1 Like

#5

World::get_sm_mut should be the following instead (note the change of the return type in the signature):

fn get_sm_mut(&mut self) -> &mut SM<'a> {
    &mut self.sm
}
5 Likes

#6

Ah, that is a very subtle distinction. I would have never realized that. Curious, what is the difference between the two syntax’s. I thought “&'a mut” was saying the same thing as “&mut XYZ<'a>”.
I guess I still need to dig deeper into reference semantics.

Thanks vitalyd.

0 Likes

#7

Yeah, this is due to how lifetime elision works.

fn get_sm_mut(&mut self) -> &'a mut SM {
        &mut self.sm
}

That’s what you started with, and this ends up being the same as:

fn get_sm_mut(&'a mut self) -> &'a mut SM<'a> {
        &mut self.sm
}

So you end up mutably borrowing self for 'a, and 'a is longer-lived than World itself - that ends up borrowing/“locking” w for its entire lifetime, yielding the compiler error you saw.

What you had really intended (and hence the fix) is to do (with explicit lifetime here instead of elision):

fn get_sm_mut<'elided>(&'elided mut self) -> &'elided mut SM<'a> {
        &mut self.sm
}

This says borrow self mutably for as short of a lifetime as necessary (determined by the compiler at the callsite), and return a similarly lifetimed mutable borrow of SM<'a>. In this case, the mutable borrow returned from this method ends inside the scope you had:

    {
        let sm = w.get_sm_mut();
        sm.change(3.0);
    }

and you’re free to borrow w again thereafter.

Have a look at https://doc.rust-lang.org/reference/lifetime-elision.html for more info on elision rules. Once you know them, they’re helpful in making code more ergonomic but they can trip you up in the beginning.

3 Likes

#8

Nice. Now that makes sense. :ok_hand:

Thanks again vitalyd.

0 Likes

#9

Yep, you had a reference of a struct that contained another reference, and if you don’t pay attention to lifetime elision you can end up borrowing for the bigger lifetime instead of shorter ones.

Tip: try to call structs lifetime parameters with something related to their field names instead of 'a (or 'b), since the latter are usually used for function signatures to explicit how to “connect” the input and output lifetimes.

In your case, for the SM struct, I’d have named the lifetime parameter 'scenes.

1 Like