Lifetimes Question

Hi, I would appreciate any insight anyone can give me into the following situation.

Consider the code:

trait Jacket<'a> {}
struct Pocket<'a, J: Jacket<'a>>(&'a J);

fn fail<'a, J: 'a + Jacket<'a>>(j: J) {
    Pocket(&j);
}

I don't quite understand why I get this error:

21 | fn fail<'a, J: 'a + Jacket<'a>>(j: J) {
   |         -- lifetime `'a` defined here
22 |     Pocket(&j);
   |            ^^
   |            |
   |            borrowed value does not live long enough
   |            requires that `j` is borrowed for `'a`
23 | }
   | - `j` dropped here while still borrowed

Haven't I declared that J must have a lifetime of at least 'a? How can it be that j doesn't live long enough to refer to it (and then immediately drop the reference)?

Thanks in advance for your help :bowing_man:

There are two fundamental approaches to ensure that references always point to a live object. Garbage-collected languages extend the validity region of objects until after the last reference is destroyed. Rust instead invalidates references whenever an object moves or is destroyed, and lifetimes are just the mechanism to do this. Your lifetime annotations need to match what the code actually does, or else the compiler will reject your program.

By taking j by value, you're moving it into the function's local scope, which requires it to be either destroyed or moved elsewhere before the function exits. When you take a reference to a local variable, like &j, it will always get a new lifetime that expires when that local is moved or destroyed. This is the only way to keep both the invariant that references are always valid and direct programmer control over when and where objects are stored and destroyed.

3 Likes

I get that &j gets a new lifetime which expires when j is moved or destroyed, but what does it mean that J: 'a then? Also, why does the following compile, if that's the case?


trait Jacket {}
struct Pocket<'a, J: Jacket>(&'a J);

fn fail<'a, J: 'a + Jacket>(j: J) {
    Pocket(&j);
}

If I remove the lifetime from the trait, I still have the same lifetime bound on the reference in the struct, so why doesn't it complain that j doesn't live long enough anymore? In fact, it compiles with or without the J: 'a if there's no trait bound.

1 Like

Lifetime parameters on traits are invariant. Lifetime parameters on structs depend on how the lifetimes are used (within the definition itself). So when you have a lifetime on your trait, and then put a trait bound on your struct with that lifetime, your struct becomes invariant.

When you remove the lifetime parameter from the trait, the only lifetime is introduced by the struct, and there's no lifetime in the bound to make it invariant.

Here's another alternative which compiles. Without the bound, your struct is covariant (like the reference is), and thus a local variable with a shorter lifetime can be created.

trait Jacket<'a> {}
struct Pocket<'a, J>(&'a J);

fn no_fail<'a, J: 'a + Jacket<'a>>(j: J) {
    Pocket(&j);
}
3 Likes

Thanks these answers get me pretty far. I'm still a little confused on why the compiler hints me to add J: 'a if I leave it off when I keep the trait bound, and what J: 'a actually means in this context, given that this also compiles:

trait Jacket<'a> {}
struct Pocket<'a, J>(&'a J);

fn fail<'a, J: Jacket<'a>>(j: J) {
    Pocket(&j);
}

J:'a generally means that all references stored inside the type J will be valid for at least the lifetime 'a, but it doesn’t tell you anything about how long any particular instance of J will stick around.

2 Likes

You mean in this version?

trait Jacket<'a> {}
struct Pocket<'a, J: Jacket<'a>>(&'a J);

fn fail<'a, J: Jacket<'a>>(j: J) {
    Pocket(&j);
}

The definition of Pocket says that you can only store a &'a Jacket<'a> where the lifetimes match, and the signature of fail says you're getting a Jacket<'a>, so when you make the local Pocket you must be constructing a Pocket(&'a Jacket<'a>).

But no where is there anything that says J: 'a. Maybe J doesn't live for 'a but still implements Jacket<'a>.

Let's try a more concrete example. Consider this:

impl<'a> Jacket<'static> for &'a () {}

fn main() {
    let local = ();
    fail::<'static, _>(&());
    fail::<'static, _>(&local);
}

Without the lifetime bound, neither call to fail errors. And if you then comment out the construction of Pocket, it compiles. There's no restrictions on the lifetime of the parameter, so both calls are okay. But clearly it would be unsound to create a &'static reference to the non-'static data, which is what the construction of Pocket tries to do.

With the lifetime bound, the first call is still ok (the constant () is static and a static ref is produced), but the second call errors (with or without the construction of the Pocket.)

2 Likes

Thanks, these answers give me a lot more insight (and a lot to think about)!

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.