What is the point of lifetime parameters in "struct" & "impl" blocks?


#1

Hi.
Correct me if I’m wrong. In this code:

struct Foo<'a>(&'a [i32])

We tell rust to check to see if struct foo lasts as long as its reference.
but what is the meaning of a code like this?

struct foo<'a, 'b>(&'a [i32], &'b i32)

if it doesn’t mean anything, why do we need lifetime specifiers in structs anyway? Rust can just check to see if references inside any struct live long enough. what is the point of declaring only one lifetime ( lets say 'a ) and the puting it before every reference?

Or inside impl block, in this code:

impl<'a> From<&'a [i32]> for Foo

what extra information do we give rust? what does it even mean to declare a lifetime and use it without giving information about lifetime?


#2

We tell Rust that the struct is valid over some region/lifetime 'a that the caller/user determines. In practical terms, it means the struct cannot outlive that lifetime but can live for a shorter one.

Same thing except two distinct lifetimes/regions. Two (or more) distinct lifetime parameters become more important when you have mutable references inside.

Edit: Let me elaborate a bit (I’m on mobile so need a few iterations to fully respond :slight_smile: )

You should think of lifetime parameters like generic type parameters - eg struct Foo<T>. Where the type parameter describes something about a type (or maybe nothing at all, if there are no bounds), lifetime parameters describe something about the references the struct holds. Generic lifetime parameters have fewer bounds options than generic type parameters. If you have 1 lifetime parameter, you pretty much can’t say anything else about it. If you have two or more, however, you can express an “outlives” relationship between them - eg 'a: 'b. The other difference is that concrete lifetimes are filled in by the compiler, whereas generic type parameters are filled in/chosen by user code explicitly.

For a struct holding immutable references you can generally get away with a single lifetime parameter for all reference fields in it. The real references can live for different concrete lifetimes but the lifetimes can be “squeezed” down. More precisely, immutable references have a subtyping relationship (aka variance) - a longer lifetime is a subtype of a shorter lifetime, and can be substituted in places where the shorter lifetime is needed. Mutable references, however, do not have this subtyping relationship - they’re invariant. So if you have a struct with, say, one mutable reference with a lifetime 'a, then making other references in that struct have 'a as well will not allow them to have a longer lifetime. That makes sense if you think about it - if this were allowed, then you could potentially set a longer lived reference to point to something that lives for a shorter lifetime, and end up with a dangling reference. So to avoid this, and to allow lifetimes to vary across the reference fields, you would specify a different lifetime parameter for the other references. The compiler then treats them as distinct and allows (lifetime) subtyping.

Let me know if something could use more explanation.


#3

Thanks. Great explantion but still confuses me


#4

Which part(s)? Happy to try and clear some things up (if I can).


#5

So why there is no ellision rules for this code:

impl<'a> From<&'a [i32]> for Foo

Why do we need lifetime parameter here? What does it tell rust that it cannot dudce itself?


#6

In this particular example it doesn’t say anything that it couldn’t deduce itself. There’s been some talk about eliding lifetimes in more places, such as a struct with a single one: https://internals.rust-lang.org/t/lang-team-minutes-elision-2-0/5182

Thus far Rust has preferred to err on the side of explicitness, modulo a few areas that were deemed an ergonomic hit without some implicitness (existing lifetime elision being the poster child). It’s likely this will expand, as that internals thread hints at.


#7

So if you have a struct with, say, one mutable reference with a lifetime 'a, then making other references in that struct have 'a as well will not allow them to have a longer lifetime.

So, In that case, Why does this code compile?

struct Foo<'a> {
    x: &'a mut i32,
    y: &'a i32,
}

fn main() {
    let y = &20;
    {
        let x = &mut 10;
        let foo = Foo { x: x, y: y };
    }
}

Can you give me an example?


#8

Good question - let me elaborate a bit with a couple of examples.

struct Foo<'a> {
    x: &'a i32,
}

struct Bar<'a> {
    f1: &'a mut Foo<'a>,
    f2: &'a Foo<'a>,
}

fn main() {
    let x = &20;
    let mut foo = Foo { x };

    let x2 = &10;

    let mut foo2 = Foo { x: x2 };
    let bar = Bar {
        f1: &mut foo,
        f2: &foo2,
    };
}

This example won’t compile. But this one will:

struct Foo<'a> {
    x: &'a i32,
}

struct Bar<'a, 'b> {
    f1: &'a mut Foo<'a>,
    f2: &'b Foo<'b>,
}

fn main() {
    let x = &20;
    let mut foo = Foo { x };

    let x2 = &10;

    let mut foo2 = Foo { x: x2 };
    let bar = Bar {
        f1: &mut foo,
        f2: &foo2,
    };
}

Note the two distinct lifetime parameters in Bar in the latter example.

When you have &'a mut T, for some type T, the invariance is over the T - not over the 'a (this can still be variant). Let me quote the nomicon on this fairly subtle but important distinction:

From: https://doc.rust-lang.org/beta/nomicon/subtyping.html#variance

In your original case of &'a mut i32, you can see the 'a being variant. i32 is a 'static type (no lifetimes itself) so the invariance of T doesn’t come into play.

Happy to try explaining more.


#9

An elision rule for impl headers was specified in RFC 141 but it hasn’t been implemented yet.