Understanding Borrow Checker On Function Call

Hi all, I'm trying to understand how the borrow checker work and tried this code, but I can't figure out why the compiler behaves like this:

use std::marker::PhantomData;

struct X<'a> {
    x: PhantomData<&'a i32>,
}

impl<'a> X<'a> {
    pub fn new() -> Self {
        Self { x: PhantomData }
    }

    pub fn method1(&'a mut self) {}

    pub fn method2(&mut self) {}
}

fn main() {
    let mut x = X::new();
    let y = &mut x;
    
    X::method2(y); // can I say that y is a &'b mut X<'a> where 'b:'a ?
    X::method2(y);
    
    X::method1(y); // why this one success? Shouldn't this failed?
    X::method1(y); // and why this one failed?
}

(Playground)

My questions are:

  1. Why calling method1 once doesn't give me compile error? Shouldn't it compile error because there is mutable reference and immutable reference to 'a ?
  2. Why the second call to method2 gives error?
impl<'a> X<'a> {
    pub fn method1(&'a mut self) {} // X::method1(&'a mut X<'a>)
    pub fn method2(&mut self) {} // X::method2(&'b mut X<'a>)
}

fn main() {
    let mut x = X::new(); // x: X<'1>
    let y = &mut x; // y: &'1 mut X<'1>
    
    X::method2(y); // reborrow: X::method2(&'2 mut X<'1>) where '1: '2
    X::method2(y); // reborrow: X::method2(&'3 mut X<'1>) where '1: '3
    
    X::method1(y); // X::method1(&'1 mut X<'1>): &'1 mut X<'1> is comsumed, so you won't be able to use it anymore
    X::method1(y); // cannot borrow `*y` as mutable more than once at a time
    
    // And you can't use any of x, &x or &mut x
    //x;
    //&x;
    //&mut x;
}
1 Like

@vague why can't I reborrow x for the second time just like when calling method2?

The signature prevents it from reborrowing. method1 requires &'1 mut X<'1>, so the lifetime can't be
any other one.

reborrowing, though it's really a special case of autoborrow. The idea here is that when we see a parameter of type &'a T or &'a mut T we always "reborrow" it, effectively converting to &'b T or &'b mut T. While both are borrowed pointers, the reborrowed version has a different (generally shorter) lifetime.
src: niko's post in 2013

Invariance. A &mut T<'long> can't be shrank to a &mut T<'short>, because this would lead to a dangling pointer if you write through the reference. Thus, &'a mut T<'a> is only ever that exact type, borrowing the value for the entirety of its lifetime, thereby rendering it useless.

This is a well-known antipattern, and usually so is an explicit lifetime annotation on &self and especially &mut self.

3 Likes

Let me point out a few more things...

It can succeed once because the compiler just has to be able to find a lifetime 'x such that...

  • x has type X<'x>
  • y has type &'x mut X<'x>
  • y isn't dangling -- so x is still around
  • The lifetime includes any place these values are used.

And any lifetime that includes the calls to method2 and the first call to method1 will work. (I'm ignoring the attempt at a second call to method1 since that is an error.) The lifetime could end right after the first call to method1, or it could be as long as the body of main (until x goes away).

As was already noted, once you hit the first call to method1, y and x are totally locked up and you can't use them anymore.

y is a &'x mut X<'x> as per the above. But &mut references are also not Copy. So how can you use them more than once at all? Didn't you give it away? Not quite: when you call a method that takes a &'a mut, the compiler automatically inserts a reborrow. The reborrow can be shorter than 'a because &'a mut T is covariant in 'a, even though it is invariant in T.

So although y is a &'x mut X<'x>, here you use a reborrow with type &'b mut X<'x>, where 'b is shorter than 'x. Once the reborrow lifetime ends, you can use y again. Because you can use y again on the next line, you know the reborrow lasts just the one line, in fact.

(Note that it's 'x: 'b ("'x outlives 'b", "'x is valid for all of 'b"), not the other way around.)

Then you do it again for the second call to method2.

But for the call to method1, any reborrow has to also be &'x mut X<'x> because

  • The signature says the lifetimes have to match, unlike method2
  • Invariance, as others pointed out -- the inner 'a isn't allowed to change

The outer lifetime is forced to be 'a to satisfy those two points.

Structs can own values that have lifetimes, and you can take &muts to those outer structs. Let's say you had an actual &'a i32 in there. X owns the reference -- the value -- but not what it points to. The i32 is shared -- there's a &i32, a shared reference, pointing at it. Shared references like &i32 are Copy, so there could be 100 things pointing at it.

So you can take a &mut X<'a>, and that implies exclusive access to the reference -- the one copy of the reference inside the X, that is. But because &i32 is Copy, you could create a copy outside of X, even through the &mut X.

So one could say that the aliasing guarantee stops when you "cross" the shared reference. But if it helps, instead think of accessing the i32 through a &mut &i32 as taking a copy of the shared reference when you hit one, so that it's no longer under the guarantee of the &mut.

You can't get a &mut i32 out of a &mut i32 &i32, in any case.


Final note for now: the aliasing guarantees are more nuanced than "if there's a &mut to something, there are no & and no other &mut to that thing" -- as the reborrows illustrate. When a &mut is usable, there must be no other live references -- or phrased differently, when a &mut is used, it cancels the validity of any reborrows. Similar to how if you move a value, any references to it are no longer valid.

2 Likes

Hmm, isn't this common pattern for creating tree data structure using arena?

Ah, this one is very clear, thank you.
@quinedot Can I say it like this:

let mut x = X::new();
let y = &mut x;
X::method2(y); // a new kind of y is created with a shorter lifetime 'b, where 'x:'b
               // that "new kind of y" is moved to method2 because it doesn't implement copy
               // the original y however, is intact.
               // after returned, that "new kind of y" is "kind of" dropped.
X::method2(y); // the same thing happen. 
               // We can use y here because y is never been moved
               // The thing that was moved was a cloned version of y 
               // that implicitly created by rust with shorter lifetime.
X::method1(y); // y's original lifetime is 'x and method1 require &'x mut
               // we can't make shorter version of y because of the rqeuired lifetime is 'x
               // so we "kind of" move the y to method1.
X::method1(y); // we can't do this because y is already "kind of" moved. 

More or less -- a reborrow of y is passed to method2 with a shorter outer lifetime. It's never returned, so it drops in method2 ... but the lifetimes of borrows still matter after the values drop, and it's the lifetimes that matter for borrow check errors. (You can have a local variable with type &'static str, that doesn't mean the variable never drops.)

Reborrowed, not cloned. The previous reborrow's lifetime has expired so you can use y again (reborrow it again).

I think it's technically still a reborrow, but the reborrow happens to have the same maximum lifetime (or the errors would be different)... but you can't use y (or x) any more once you make this call, so it's similar.

You can't use y until the reborrow's lifetime is up, but that's at the same point y's lifetime is up. So you can't use y again. And you gave away the reborrow to method1 (and it didn't give it back), so you've completely lost access to x too.

Yes, or in more detail:

  • Given an x: X<'a>
  • Once you create a y: &'a mut X<'a>
    • You can only use x through that y (including reborrows)
  • If you create a reborrow &'a mut X<'a> from y, then you can only use x through that
    • And y is unusable because the exclusive reborrow has the same lifetime

In your program,

  • You create x and then the y at the top, with type &'a mut X<'a>
    • Now x is only usable through y or its reborrows
  • You use y for a short reborrow to call method2
  • You use y for a short reborrow to call method2
  • You use y for a reborrow of &'a mut X<'a> to call method1, making y unusable
    • (The reborrow doesn't expire until y expires so y never becomes usable again)
  • method1 doesn't return any reborrow of that, so there's no way to use x anymore either
1 Like

It must not be. A &'a mut T<'a> is useless (more precisely, unusable) for the reasons enumerated above. You are probably misunderstanding the interface of a particular arena crate you are using.

Oh, I meant &'a T<'a> actually.

Ah yes, roborrowed, and dropped at method2.
Yeah, local variable with &'static T type is dropped just like other variable. But the T will still live in 'static lifetime. Only the reference is dropped.

Right, this makes more sense actually. Thank you

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.