Trying to understand lifetime

The following codes(playground) has compile error:

trait ToCalc {
    type T<'l>;
    fn to_calc<'l>(&self, data: Self::T<'l>);
}

trait ForCalc {
    fn for_calc<'a>(&self, data: &'a Vec<f32>);
}

impl<'l, T> ForCalc for T
where
    T: ToCalc<T<'l> = &'l Vec<f32>>,
{
    fn for_calc<'a>(&self, data: &'a Vec<f32>) {
        self.to_calc(self, data);
    }
}

Error: lifetime may not live long enough
    ╭─[command_13:1:1]
    │
 10 │ impl<'l, T> ForCalc for T
    ·      ─┬  
    ·       ╰── lifetime `'l` defined here
    · 
 14 │     fn for_calc<'a>(&self, data: &'a Vec<f32>) {
    ·                 ─┬  
    ·                  ╰── lifetime `'a` defined here
 15 │         self.to_calc(data);
    ·         ─────────┬────────  
    ·                  ╰────────── argument requires that `'a` must outlive `'l`
────╯

I am trying to understand the error in this way:
The method for_calc needs the argument data : &'a Vec<f32>, but the method self.to_calc needs the data: &'l Vec<f32>. Only when 'a: 'l, &'l Vec<f32> can be a subtype of &'a Vec<f32>. So if I change the trait bound to for<'l> T: ToCalc<T<'l> = &'l Vec<f32>>, , the codes can compile.

But if I keep the error codes and assuming it can compile. If I apply it in concrete type:

struct Foo;

impl ToCalc for Foo {
    type T<'l> = &'l Vec<f32>;
    fn calc<'l>(&self, data: Self::T<'l>) {}
}

Next I call:

let data = vec![1f32, 2., 3.];
let data_ref = &data;
Foo.for_calc(data_ref)

Then, data_ref is the &'a data, so 'a = the lifetime of data_ref. But, what is the 'l?

It's the other way round. &'l T is a subtype of &'a T if and only if 'l: 'a (read: "'l outlives 'a").

1 Like

I'll see if I can get back to your actual questions later, but

-impl<'l, T> ForCalc for T
+impl<T> ForCalc for T
 where
-    T: ToCalc<T<'l> = &'l Vec<f32>>,
+    T: for<'l> ToCalc<T<'l> = &'l Vec<f32>>,

Playground.

(The 'a on for_calc doesn't need to be explicit either.)

1 Like

Still haven't looked at your questions :wink: but the suggestion is not great...

   = help: consider adding the following bound: `'a: 'l`

...because adding that bound is only possible on the method, which would make it not match the trait, and you can't add it to the trait without parameterizing the trait with a lifetime, and doing that will probably cause headaches or impossibilities elsewhere.

So with the fact that the help is unhelpful in mind, let's move on to your questions.


Oh, you already knew the solution! Ha, sorry.

From the perspective of implementing ForCalc, the method for_calc doesn't need a specific argument, it must accept any lifetime 'a. But the method self.to_calc needs the data &'l Vec<f32>, for some specific 'l -- the way you defined the implementation,

impl<'l, T> ForCalc for T where ...

the body of the implementation can only "see" one concrete lifetime, 'l. In particular it only knows

... ToCalc<T<'l> = &'l Vec<f32>>

for that one lifetime 'l; it doesn't know it holds for all lifetimes. [1] So you can only effectively call to_calc::<'l>. You can't call to_calc::<'x> whether it's longer or shorter, because for all the compiler cares to reason about, that might take a T<'x> = &'x String for an argument, or might not even be defined. [2]

Therefore your for_calc can't manage any lifetime 'a, only ones where &'a Vec<f32> can coerce to &'l Vec<f32>. Only when 'a: 'l as you said. The version that works, works because you now have an implementation that knows about the relationship for all lifetimes within one implementation body.

for<'l> ToCalc<T<'l> = &'l Vec<f32>>

Now it knows you can call to_calc::<'_> with any lifetime, so you can call to_calc::<'a> for any intput lifetime 'a. (You can technically call to_calc::<'anything_shorter_than_a> too.)


    let data_ref = &data;    // 'x
    Foo.for_calc(data_ref);  // for_calc::<'y>
    /*
    // body of Foo::for_calc called with data: &'y Vec<_>
    //    self.to_calc(data) // Foo::to_calc<'z>
    */

In the compiling version, so long as 'x: 'y and 'y: 'z and 'x is shorter than the next use or drop of data, any lifetimes will work. You can think of each one being shorter than the previous or you can think of them all being the same ('x), either way works.

In the erroring version... I'm not sure how useful it is to think about because the constraints can't be satisfied without changing something. If you had the <'a: 'l> bound, it'd all be the same reasoning. If you made 'a a trait parameter instead of 'l and made things compile that way, 'a becomes invariant, which in the above code block would mean 'y == 'z. But there's no practical difference in this particular case -- adding a lifetime parameter to the trait is much more practically impactful.


Hopefully that was more helpful than confusing.


  1. I think that has to be the case given all the impls and traits together, and maybe Rust will be able to reason across binder levels like this eventually, but so far it cannot. I'm not sure which is less confusing honestly. There are a lot of subtleties, especially when T is not 'static. ↩︎

  2. As the compiler is now. ↩︎

1 Like

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.