zareami10
zareami10

Reputation: 301

Questions on understanding lifetimes

I have been having a hard time understanding lifetimes and I would appreciate some help understanding some of the subtleties which are usually missing from the resources and other question/answers on here. Even the whole section on the Book is misleading as its main example used as the rationale behind lifetimes is more or less false (i.e. the compiler can very easily infer the lifetimes on the mentioned function).


Having this function (kinda similar to the book) as an example:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    x
}

My understanding is that the explicit lifetimes asserts that the returning reference should not live longer than the shortest of the lifetimes of x and y. Or in other words, both x and y should outlive the returning reference. (Although I'm completely unsure of what exactly the compiler does, does it check the lifetimes of the arguments and then compares the minimum with with life time of the returning reference?)

But then what would the lifetime mean if we have no return values? Does it imply a special meaning (e.g. compared to using two different lifetimes?)

fn foo<'a>(x: &'a str, y: &'a str) {
    
}

And then we have structs such as:

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

And

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

It seems using the same lifetime for the fields adds some constraints, but what exactly is that constraint which causes some examples not to work?


This one might require a question of its own, but there are a lot of mentions of lifetimes and scopes being different but without elaborating much, are there any resources delving deeper on that, especially also considering non-lexical lifetimes?

Upvotes: 3

Views: 216

Answers (1)

rodrigo
rodrigo

Reputation: 98436

To understand lifetimes you have to note that they are actually part of the type, not of the value. This is why they are specified as generic parameters.

That is, when you write:

fn test(a: &i32) {
    let i: i32 = 0;
    let b: &i32 = &i;
    let c: &'static i32 = &0;
}

then variables a, b and c are actually of different types: one type is &'__unnamed_1 i32, the other is &_unnamed_2 i32 and the other is &'static i32.

The funny thing is that lifetimes create a type hierarchy, so that when a type lives longer than another type, but except that they are the same, then the long-lived one is a sub-type of the short-lived one.

In particular, in a case of extreme multiple inheritance, the &'static i32 type is a subtype of any other &'_ i32.

You can check that Rust sub-types are real with this example:

fn test(mut a: &i32) {
    let i: i32 = 0;
    let mut b: &i32 = &i;
    let mut c: &'static i32 = &0;
    //c is a subtype of a and b
    //a is a subtype of b
    a = c; // ok
    c = a; // error
    b = a; // ok
    a = b; // error
}

It is worth noting that lifetimes are a borrow checker issue. Once it is satisfied and the code is proven to be safe, lifetimes are erased and the code generation is done blindly, assuming that all accesses to memory values are valid. This is why even though life times are generic parameters, foo<'a>() is only instantiated once.

Going back to your examples:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    x
}

You can call this function with values of different lifetimes, because the compiler will deduce 'a as the shorter of both, so that &'a str will be a super-type of the other one:

    let s = String::from("hello");
    let r = foo(&s, "world");

This is equivalent to (invented syntax for lifetime annotation):

    let s: &'s str = String::from("hello");
    let r: &'s str = foo::<'s>(&s, "world" as &'s str);

About the structures with multiple lifetimes, it generally doesn't matter, and I usually declare all lifetimes the same, particularly if the type is private to my crate.

But for public generic types, it may be useful to declare several lifetimes, particularly because the user may want to make some of them 'static.

For example:

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

struct Bar<'a> {
    x: &'a i32,
    y: &'a str,
}

The user of Foo may use it as:

let x = 42;
let foo = Foo { x: &x, y: "hello" };

But the user of Bar must allocate a String, or do some stack-allocated str wizardry:

let x = 42;
let y = String::from("hello");
let bar = Bar { x: &x, y: &y };

Note that Foo can be used just like Bar but not the other way around.

Also the impl Foo may provide additional functions that are not possible for impl Bar:

impl<'a> Foo<'a, 'static> {
    fn get_static_str(&self) -> &'static str {
        self.y
    }
}

Upvotes: 6

Related Questions