Why doesn't this code compile without separate lifetime parameters?

I would like to have this code compiled, but it could not be compiled with an error

    struct Inner<'a> {
        reference: &'a (),
    }
    impl<'a> Drop for Inner<'a> {
        fn drop(&mut self) {
        }
    }
    
    struct Outer1<'a> {
        inner: &'a mut Inner<'a>,
    }

    let mut inner = Inner { reference: &mut (), };
    Outer1 { inner: &mut inner, };
   Compiling playground v0.0.1 (/playground)
error[E0597]: `inner` does not live long enough
  --> src/main.rs:18:21
   |
18 |     Outer1 { inner: &mut inner, };
   |                     ^^^^^^^^^^ borrowed value does not live long enough
19 | }
   | -
   | |
   | `inner` dropped here while still borrowed
   | borrow might be used here, when `inner` is dropped and runs the `Drop` code for type `Inner`

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

I noticed that if I change the lifetime parameter in Outer1 like this, it compiles

    struct Outer1<'a, 'b> {
        inner: &'a mut Inner<'b>,
    }

I do not understand why the first code does not compile. Why is this?

&'a mut Inner<'a> is a well-recognized anti-pattern. Since mutable references are invariant in their lifetime, it means that the value must be borrowed precisely for the entirety of its lifetime. This can lead to weird errors, including the impossibility to drop such a value.

6 Likes

Thanks for the answer. I now know that this is an anti-pattern. I'm still not sure about the variance and have a question. Am I correct that &'a mut Inner<'a> is a well-recognized anti-pattern, but &'a mut Inner<'b> and &'a Inner<'a> are not?

&'a mut Inner<'b> is not an anti-pattern. With &'a mut Inner<'b>, 'a is able to be shorter than 'b -- for example as short as a method call. The outer lifetime is often elided and the inner lifetime may be behind an alias like self.

impl<'b> Inner<'b> {
    // Various amounts of elision and aliases
    fn foo_1<'a>(inner: &'a mut Inner<'b>) { /* ... */ }
    fn foo_2<'a>(inner: &'a mut Self) { /* ... */ }
    fn foo_3(inner: &mut Inner<'b>) { /* ... */ }
    fn foo_4(inner: &mut Self) { /* ... */ }
    // Same but for a method taking `&mut self`
    fn bar_1<'a>(&'a mut self) { /* ... */ }
    fn bar_2(&mut self) { /* ... */ }

The most common of these is probably &mut self, where you don't even really think about it. In all cases, eliding the lifetime gets you a distinct one.


&'a Inner<'a> can sometimes be less flexible than &'a Inner<'b>, but it often doesn't matter. If Inner is invariant over its lifetime parameter, than you may run into similar issues with &'a Inner<'a> as you do with &'a mut Inner<'a>, so having separate parameters is the safer bet. And, like with the example above, just not naming the parameter gets you the two lifetimes "for free" anyway.

I say "it often doesn't matter" because if Inner is covariant over the lifetime parameter, then it can "shrink" automatically where it usually matters -- for example in a method call, you may end up with a &'a Inner<'a> that only lasts the length of the call, even if you called it on a Inner<'static>, say.

But even this might still be too inflexible if you actually need to preserve the inner lifetime for some reason though. For example, since shared references are Copy, you can get (a copy of the) &'static str reference out from the inside of a &'short &'static str.

pub fn quz(s: &/* 'short */ &'static str) -> &'static str {
    *s
}

So I guess in summary I'd say that &'a Inner<'a> isn't the red flag that &'a mut Inner<'a> is, and it's less likely to be a problem because Rust tries hard to find some interpretation that works and that is easier to achieve with covariant lifetimes, but it's often still a questionable choice (for example in a method signature where you could get two lifetimes just via elision). It might be sensible if you're storing a &'a Inner<'a> in another struct and you want to avoid two lifetime parameters or the like (provided the inner lifetime is covariant and not significant to preserve).

2 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.