Allowed non-related lifetime annotation


#1

Why is it allowed to annotate a reference with a lifetime that does not correspond to the referenced resource?
In this example, x2 references the resource r2 that has a smaller lifetime than 'a, which is the lifetime of the resource r1.

#[derive(Debug)]
struct Foo {
    f : i32
}

#[allow(unused_variables)]
fn m2<'a>(x1: &'a Foo, x2: &'a Foo) -> &'a Foo {
    x1
}

fn m1<'a>(x1: &'a Foo) {
    let r2 = Foo { f : 77 };
    
    let t = m2(&x1, &r2);
    
    println!("t: {:?}", t);
}

fn main() {
    let r1 = Foo { f : 33 };
    
    m1(&r1);
}

I would expect the compiler to forbid me from annotating the reference x2 with an already defined lifetime that does not correspond to the resource that it is referencing to.

#[derive(Debug)]
struct Foo {
    f : i32
}

#[allow(unused_variables)]
fn m2<'a, 'b>(x1: &'a Foo, x2: &'b Foo) -> &'a Foo {
    x1
}

fn m1<'a>(x1: &'a Foo) {
    let r2 = Foo { f : 77 };
    
    let t = m2(&x1, &r2);
    
    println!("t: {:?}", t);
}

fn main() {
    let r1 = Foo { f : 33 };
    
    m1(&r1);
}

Could you tell me what I am missing please?


#2

Let’s say r1 has lifetime 'r1 and r2 has 'r2. 'r1 outlives 'r2 (we can express this as 'r1: 'r2). Because of lifetime covariance (or is it contravariance?) &'r1 Foo is valid where a &'r2 Foo is required. So in the first example 'a in m2 resolves to the lifetime of r2 and x1 outlives that so there’s no problem.

I’m not sure you can say that a lifetime strictly corresponds to any resource.


#3

Maybe I’m wrong, but according to the documentation on rustbyexample, lifetimes are defined as follows:

“The lifetime of an object starts when the object is created and ends when it goes out of scope (i.e. it gets destroyed, because of the RAII discipline).”

“All references actually have a type signature of the form &'a T, where 'a is the lifetime of the referenced object.”

So that lifetime covariance makes the function signature confusing. Because if you have the following:

fn m2<'a>(x1: &'a Foo, x2: &'a Foo) -> &'a Foo

I would expect that function to get two references to Foo with the same scope. Am I missing anything?


#4

[quote=“alvalea, post:3, topic:744”]
“All references actually have a type signature of the form &'a T, where 'a is the lifetime of the referenced object.”
[/quote]I’d say this isn’t the lifetime of the object but a lifetime satisfied by the object’s lifetime.

[quote=“alvalea, post:3, topic:744”]
I would expect that function to get two references to Foo with the same scope.
[/quote]I wouldn’t equate a lifetime with a scope. There’s an unknown lifetime 'a here (the specific lifetime inferred at the call site) and both parameters live for at least as long (but could both be e.g. 'static) allowing us to return something that lives as long as 'a (but possibly more as well).


#5

Only if we change the example to make m1 return a reference too, do distinct lifetimes for the m2's arguments become a necessity:

#[derive(Debug)]
struct Foo {
    f : i32
}

#[allow(unused_variables)]
fn m2<'a, 'b>(x1: &'a Foo, x2: &'b Foo) -> &'a Foo {
    x1
}

fn m1<'a>(x1: &'a Foo) -> &'a i32 {
    let r2 = Foo { f : 77 };
    
    let t = m2(&x1, &r2);
    
    println!("t: {:?}", t);
    
    &t.f
}

fn main() {
    let r1 = Foo { f : 33 };
    
    println!("f: {}", m1(&r1));
}

#6

Ok, I see. So maybe the key point here is the scope of the returned reference, isn’t it? I suppose that if we have a function signature like the following:

fn m2<'a>(x1: &'a Foo, x2: &'a Foo) -> &'a Foo

I guess that the resources referenced by the parameters must live at least as much as the returned reference:

fn m2<'a>(y1: &'a Foo, y2: &'a Foo) -> &'a Foo {
   y1
}

fn m1<'a>(x1: &'a Foo) {
    let r2 = Foo { f : 77 };
    let t = m2(&x1, &r2);
}

fn main() {
    let r1 = Foo { f : 33 };
    m1(&r1);
}

So we have the following:

                            { r1 { x1 r2 t { y1 y2 } } }
resource r1 scope             <------------------------>
resource r2 scope                     <-------------->
reference t scope                        <----------->
lifetime 'a                              <===========>

And in your example:

fn m2<'a, 'b>(y1: &'a Foo, y2: &'b Foo) -> &'a Foo {
    y1
}

fn m1<'a>(x1: &'a Foo) -> &'a i32 {
    let r2 = Foo { f : 77 };
    let t = m2(&x1, &r2);
    &t.f
}

fn main() {
    let r1 = Foo { f : 33 };
    let z  = m1(&r1);
}

The function m1 is extending the scope of the reference t, because it returns a reference to its field. We would have the following:

                            { r1 z { x1 r2 t { y1 y2 } } }
resource r1 scope             <-------------------------->
resource r2 scope                       <-------------->
reference t scope                <----------------------->
lifetime 'a                      <=======================>
lifetime 'b                             <==============>

Is that correct?


#7

Seems correct. It just feels weird to say ‘scope’ here, because it looks like t's ‘scope’ is larger than its lexical scope when we mean to say that t binds a reference that outlives the m1 scope.


#8

Yes, scope was not the most accurate word. Thank you very much for your clarifications.