Lifetime question: is my intuition correct?

struct Person<'a> {
    given: &'a str,
    sur: &'a mut str,
}

impl<'a> Person<'a> {
    fn given(&'a self) -> &'a str {
        self.given
    }

    fn sur_mut(&'a mut self) -> &'a mut str {
        self.sur
    }
}

fn example<'a>(mut person: Person<'a>) {
    let _one = person.given();
    let _two = person.given();
    let _three = person.sur_mut(); 
}

Hello folks, I was tweaking and trying out some different things with an example given here: quinedot rust learning guide
I understand that &'a mut &'a _ should be avoided and is pretty restrictive but, I feel like the problem isn't about that...

from what I understand this line

let _three = person.sur_mut(); 

is a reborrow of the mutable reference with a shorter lifetime, right?
but the function is annotated with a lifetime 'a and therefore expects the borrow from the struct to live as long 'a, is my intuition correct?

BTW, you'd never write anything like this in real Rust code

2 Likes

No. It is short form of

    let r: &'a mut Person<'a> = &mut person;
    let _three = Person::sur_mut(r);

Which fails on first line as the reference constructed is to a local variable so can't possibly live as long as a lifetime coming into the function.

2 Likes

your code can be extremely simply fixed by doing

impl<'a> Person<'a> {
    fn given<'b>(&'b self) -> &'b str {
        self.given
    }

    fn sur_mut<'b>(&'b mut self) -> &'b mut str {
        self.sur
    }
}

as allows for shorter borrows, where yours only allows for borrows that last for the whole lifetime of 'a. of course an even improved version would be

impl<'a> Person<'a> {
    fn given<'b>(&'b self) -> &'a str {
        self.given
    }

    fn sur_mut<'b>(&'b mut self) -> &'b mut str {
        self.sur
    }
}

as given can be copied.
more simply written

impl<'a> Person<'a> {
    fn given(&self) -> &'a str {
        self.given
    }

    fn sur_mut(&mut self) -> &mut str {
        &mut *self.sur
    }
}
1 Like

I take this to mean you understand borrowing forever is bad, but are asking about something else going on in the code.

I'm not sure where the core confusion is coming in, so I'll cover a couple things.

Variance and the error in the OP code

Expand to see the error
error[E0597]: `person` does not live long enough
  --> src/lib.rs:19:18
   |
16 | fn example<'a>(mut person: Person<'a>) {
   |            --  ---------- binding `person` declared here
   |            |
   |            lifetime `'a` defined here
...
19 |     let _three = person.sur_mut(); 
   |                  ^^^^^^----------
   |                  |
   |                  borrowed value does not live long enough
   |                  argument requires that `person` is borrowed for `'a`
20 | }
   |  - `person` dropped here while still borrowed

That's not an error about "borrowing forever" per se, it's an error about borrowing a local for a lifetime longer than the function body. It happens because person is a local, and you would have to create a &'a mut Person<'a> to call person.sur_mut(), and 'a is longer than the function body (as all nameable lifetimes are).

Why didn't it error for person.given()? Variance. You create a &'_ Person<'_>, and the compiler determines that the two lifetimes have to be the same (due to the method call signature), but they otherwise have no constraints (other than being active during the call). Effectively you create a &'x Person<'x> and call Person::<'x>::given with it, where 'x is some lifetime that only lasts as long as the call. This is possible because &'r Person<'p> is covariant in both 'r and 'p.

Why didn't the same thing happen with sur_mut? Because &mut T is invariant in T. When you create a &'_ mut Person<'_>, the inner lifetime cannot be coerced to something shorter, so it must be a &'_ mut Person<'a>. Then the signature of the method you pass it to says the lifetimes must be the same, so it must be a &'a mut Person<'a>.

Because Person<'p> is covariant in 'p, if you provide an opportunity to coerce person to a Person<'some_shorter_lifetime> before creating the &mut Person<'_>...

 fn example<'a>(mut person: Person<'a>) {
+    let mut person = person;
     let _one = person.given();
     let _two = person.given();
     let _three = person.sur_mut(); 
 }

...the error about borrowing a local too long goes away (but you would still get an error if you tried to use person after the call to sur_mut, because borrowing forever).

Reborrows

In the snippet, the body of sur_mut returns a reborrow of *person.sur. The lifetime in the type cannot be shorter due to the function signature, but that's not the point of this part of my reply.

The point I will try to make is that _three does not act the same as if you had done the reborrow inline. You can have a function like so:

fn deconstruct<'a>(person: Person<'a>) -> &'a mut str {
    let reborrow: &'a mut str = &mut *person.sur;
    reborrow
}

And it will compile. So reborrow is not a borrow of person.sur or of person -- those both went out of scope at the end of deconstruct. It's a (re-)borrow of *person.sur.

There are still checks that the reborrow can't have a lifetime longer than that of person.sur, that person.sur and person as a whole aren't usable while the exclusive reborrow exists, and so on. But the fact that these other paths are not being borrowed themselves is a key part of how reborrows work.

And there is no way to make a method signature that works the same way, where you take some outer path but only reborrow some inner path. Even though the body of sur_mut is returning a reborrow, person remains exclusively borrowed for 'a. Why? Because you had to create &'a mut Person<'a> to call the method in the first place. And the creation of that outer reference itself is the problem. (This is basically what @jonh said, in long form.)

This change to the OP compiles:

 fn example<'a>(mut person: Person<'a>) {
     let _one = person.given();
     let _two = person.given();
-    let _three = person.sur_mut();
+    let _three: &'a mut str = &mut *person.sur;
 }

Due to the differences between creating the reborrow inline and calling the method.

  • With a reborrow: person and person.sur are not themselves borrowed; you did not have to create a &mut Person<'_> or &mut &mut str.

  • With the method call: you must create a &'a mut Person<'a>, which leads to the "local borrowed too long" error.

3 Likes

Thank you for always being there to answer my questions. Your explanations throughout the forum have helped me gain a better understanding of the borrow checker and lifetimes :slight_smile:

I have some other questions related to what you’ve written in the rust learning guide, is it okay if I ask them here?

1 Like

Sure, that's okay.

fn main() {
    let mut s = String::from("this");
    let s_mut = &mut *s;

    let borrow = &s_mut; 

    f(borrow); // &'a &'b mut str but 'b == 'a => &'b &'b mut str

    println!("{borrow}");
}

fn f<'a: 'b, 'b: 'a>(s: &'a &'b mut str) -> &'b str {
    *s // &**s
}

why is this okay?

Nothing need be "borrowed forever" there, due to variance.

&'x T is covariant in 'x and T. &'y mut U is covariant in 'y (but invariant in U).

So &'x &'y mut str is covariant in 'x and 'y.[1]

When you call f(borrow), the argument can coerce to some &'c &'c mut str that needs only be valid for so long as the returned value is in use. This type doesn't have to be the same as borrow's type (you create a new temporary reference to pass in).

Moreover, borrows type doesn't even have to be &'a &'b mut str (where 'b is in the type of s_mut). Its inner lifetime can be something smaller than 'b, again due to the covariance.


Let's contrast this with &'a mut &'b mut str, which is covariant in 'a and invariant in 'b. You can't create a &'a mut &'b mut str unless 'b: 'a, but you need to be able to coerce to &'b mut &'b mut str which requires 'a: 'b, so you end up with borrow: &'b mut &'b mut str.

When you pass it to f, you pass in a reborrow, but that reborrow has to be for all of 'b as well. So s_mut is borrowed forever by borrow and *borrow is borrowed forever by the arg you passed to f. So both s_mut and borrow can no longer be used directly.


Back to the original code: even if we manage to create borrow: &'b &'b mut str exactly, the code compiles. s_mut does ends up borrowed forever,[2] so we can't use that directly ever again. But borrow is a shared reference: we can reborrow through it and create as many copies of it as we want, and the original will still be usable.


  1. The invariant portion is str, but that doesn't matter, as str has no sub- or super-types anyway. ↩︎

  2. as does *tmp_borrow, but I'm just using that to create borrow in this example ↩︎

1 Like