Lifetimes of lifetimes


#1

No joke here. I am interested what happens to lifetimes as function returns some value with its lifetime. I thought I understood it, but apparently not. Here is how I thought it works:

fn f1<'a>(p: &'a T1) -> T2<'a> {...} //return has the same lifetime as p
fn f2<'a>(p: &'a T2) -> T3<'a> {...} //return has the same lifetime as p

fn f3<'a>(r0: &'a T1) -> T3<'a> {
  let r1 = f1(&r0); //r1 has lifetime of r0
  let r2 = f2(&r1); //r2 has lifetime of r1 = lifetime of r0
  return r2;        //r2 thus has correct lifetime
}

But my code that tries something similar does not compile.


#2

Both types AND values have lifetimes. In this case, T1<'a>/T2<'a> have lifetimes 'a so values of these types cannot outlive 'a. However, this doesn’t mean that these values have the lifetime 'a. In this case, r1 and r2 are declared on f3's stack so their lifetimes are limited to the lifetime of that stack (i.e. the duration of the function call).


#3

There’s a difference between how long a value is valid for, and the lifetimes of any references it contains.

Let me rename the lifetimes to be able to talk about them unambiguously:

fn f1<'a>(p: &'a T1) -> T2<'a> {...} //return has the same lifetime as p
fn f2<'b>(p: &'b T2) -> T3<'b> {...} //return has the same lifetime as p

fn f3<'c>(r0: &'c T1) -> T3<'c> {
  let r1: T2<'c> = f1(r0);
  let r2: T3<'???> = f2(&r1);
  return r2;
}

The r1 holds a T2<'c>, since the r0 &T1 reference that is passed into f1 has lifetime 'c. However, this r1 is defined in the stack frame of f3, so any references to the r1 value can only last as long as the stack frame of f3. That is, the &r1 (anything derived from it) cannot leave the { ... } of f3.

In other words, the lifetime of the &T2 reference created by the &r1 expression is shorter than the lifetime of the r0 reference fed into the f3 function, so the 'b returned by f2 is shorter than 'c.


#4

Okay, that’s pretty clear. So what would be a good way to return a value, as the OP evidently wants to do?


#5

I am also interested in how to solve this conundrum. One way is making a macro, but that is more of a workaround. How do we do this properly?

I also tried Boxes, so that I move references out of stack, but no luck.


#6

It’s not possible to return values that point into a “small” stack frame, like this. The only approach I know would be taking a closure and passing the value down, instead of returning it. Alternatively one can restructure the code to not need the return value to depend on the stack frame of f3.


#7

Given that all the function does is bind a couple of variables and attempt to return one of their values, it seems like there is no point in f3 existing at all.

I’m tempted to ask what a “small” stack frame is, and what its size has to do with whether or not you can return a value, but I probably won’t understand that answer either.

Maybe I should just go back to Scheme, with all its shortcomings. I don’t think I’m smart enough to understand Rust.


#8

“Small” was irrelevant, it can be ignored. It definitely isn’t Rust jargon or anything, and had no relation to the actual in-memory size. A word like “short-lived” would’ve conveyed what I was meaning better, but really that’s also mostly irrelevant.

You can’t return values that point into the current stack frame, because they will be dangling pointers once you’ve left the function.


#9

The following code does compile:

pub struct T1;
struct T2<'a> {d: &'a T1, }
pub struct T3<'a> {pub d: &'a T1, }

fn f1<'a>(p: &'a T1) -> T2<'a> { T2{d: p} }
fn f2<'a, 'b>(p: &'a T2<'b>) -> T3<'b> { T3{d: p.d} }

pub fn f3<'a>(r0: &'a T1) -> T3<'a> {
  let r1 = f1(&r0);
  let r2 = f2(&r1);
  return r2;
}

The key here is that f2 may only need to keep a reference to the contents of its argument, and not the argument itself. Then, as @huon alluded to, it’s important in its signature to distinguish between those two different lifetimes.