What is the lifetime constraint for a reference deriving another ref?

I am under the impression that if ref2 is derived from ref1, then ref1 needs to outlive ref2, as the example:

struct Foo<'a> {
    x: &'a mut i32,
}

impl<'a> Foo<'a> {
    fn x(&mut self) -> &'a mut i32 { self.x }
}

fn main() {}

This will generate an error

error: lifetime may not live long enough
 --> src/main.rs:6:38
  |
5 | impl<'a> Foo<'a> {
  |      -- lifetime `'a` defined here
6 |     fn x(&mut self) -> &'a mut i32 { self.x }
  |          -                           ^^^^^^ associated function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`
  |          |
  |          let's call the lifetime of this reference `'1`

error: could not compile `playground` due to previous error

and deleting 'a like fn x(&mut self) -> &mut i32 { self.x } will fix it, elision rule making self outlive the returned.

But this code can be compiled:

use std::collections::HashMap;

#[derive(Copy, Clone)]
struct Function<'a> {
    name: &'a str,
}

struct Context<'a> {
    program: HashMap<&'a str, Function<'a>>,
    call_stack: Vec<Function<'a>>,
}

impl<'a> Context<'a> {
    fn get_function(&mut self, fun_name: &str) -> Result<Function<'a>, ()> {  // line 1
        self.program
            .get(fun_name)
            .map(|f| *f)
            .ok_or(())
    }

    fn call(&mut self, fun_name: &'a str) -> Result<(), ()> {
        let fun = self.get_function(fun_name)?;

        self.call_stack.push(fun);

        Ok(())
    }
}

fn main() {}

For line 1, though the returned Result<Function<'a>, ()> requires 'a, the lifetimes of self and fun_name are not required to outlive 'a. In fact, if 'a is added to self and fun_name (code), there will be an error: error[E0499]: cannot borrow self.call_stackas mutable more than once at a time.

Why Rust has different constraints for the two snippets?

Are there any documentation giving you a complete mental model of how lifetime works? Though I am sorta familiar with lifetime, It seems to surprise me from time to time

The basic principle is that accessing a &mut T from behind another layer of mutable reference can only work via reborrowing whereas your Function type or &T references allow being copied.

This has the effect, taking lifetimes into consideration, that &'short mut &'long mut T will allow you to get a &'short mut T reference only (unless you want to replace the inner reference with another one), whereas &'short mut &'long T allows you to get a longer lived &'long T.

I'm on mobile right now, I'm sure the others typing right now can provide good additional information :slight_smile:

4 Likes

Apart from @steffahn 's answer above (which explains why the mutable version doesn't work), it's nice to consider why the immutable version works. That's easy because immutable references are Copy so there's no reborrowing need to take place – you can simply copy the reference behind whatever other layer(s) of reference it was.

3 Likes

You copied the Function<'a> out from behind the &'b Function<'a>.

    fn get_function<'b>(&'b mut self, fun_name: &str) -> Result<Function<'a>, ()> {  // line 1
        self.program        // &'b HashMap<&'a str, Function<'a>>
            .get(fun_name)  // Option<&'b Function<'a>>
            .map(|f| *f)    // Option<Function<'a>>
            .ok_or(())      // Result<Function<'a>, ()>
    }

The copy is the key. Here's a more fundamental demonstration:

fn copy_inner_shared_ref<'a: 'b, 'b, T: ?Sized>(r: &'b &'a T) -> &'a T {
    *r
}

fn copy_inner_shared_ref_mut<'a: 'b, 'b, T: ?Sized>(r: &'b mut &'a T) -> &'a T {
    *r
}

&mut on the other hand is not Copy. (Sometimes it seems like it, but you're really performing a reborrow, which is its own large topic.) Because of this, you can't move a &'long mut T from outside a &'short mut &'long mut T, unless you have something to replace the &'long mut T with.

Demonstration.

In this case, your &'a mut self is a &'a mut Context<'a>. Anything inside a &mut -- here, Context<'a> -- is invariant. That means that the lifetimes within cannot be coerced to be shorter. The net result is that a &'a mut Context<'a> causes the Context<'a> to be mutably -- that is, exclusively -- borrowed for its entire remaining validity period. You can't use the Context<'a> ever again, except through something derived from that &'a mut -- like the return value of get_function. You can't move it, you can't borrow it again, and if you had a non-trivial destructor you wouldn't be able to call that either (meaning your code just wouldn't compile even without the second call attempt).

For this reason, &'a mut Thing<'a> is a red flag. It has extremely niche practical application and is pretty much never what you want.

You don't need the &'a mut to copy out a Function<'a>, and the problems with &'a mut Context<'a> are independent of that -- just creating &'a mut Context<'a> causes the difficulties, no matter what you do or don't do with it.

Bonus section!

&'a Context<'a> is covariant in Context<'a>, which is what you're probably more use to (even if you're not conscious of it) -- the lifetime can be coerced or reborrowed as a shorter lifetime. It's usually more forgiving because if Context<'a> is covariant in 'a (it is), then a &'long Context<'long> can generally be coerced to a &'short Context<'short>. However it is still technically over-restrictive and you should use &Context<'a>. And your example is actually a demonstration of when this can matter! Because you first go through a &mut Context<'a> -- even if it's not a &'a mut Context<'a> -- the 'a in Context<'a> is invariant. So subsequently calling a method requiring a &'a Context<'a> is only possible if you had started with a &'a mut Context<'a>, and this version of your example still does not compile.

Unfortunately there is no comprehensive lifetime documentation. Here's a recent thread on the topic and meta-topic.

3 Likes

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.