I don't able to understand lifetime specifiers :(

I can understand the scope of variables. But why we need to specify lifetime specifiers in strcuts? Already all references has to be in valid scope, why we define multiple lifetime specifiers again? Let me give some examples:

struct Struct1<'a> {
    prop1: &'a i32,
}

struct Struct2<'a, 'b> {
    prop1: &'a i32,
    prop2: &'b i32
}

struct Struct3<'a, 'b, 'c> {
    prop1: &'a i32,
    prop2: &'b i32,
    prop3: &'c i32,
}

struct Struct4<'a> {
    prop1: &'a i32,
    prop2: &'a i32,
    prop3: &'a i32,
    prop4: &'a i32,
}

fn main() {
    // here is scope1

    // these variables valid in scope1
    let v1 = 10_i32;
    let v2 = 20_i32;
    let struct3 = Struct3 { prop1: &v1, prop2: &v1, prop3: &v1 };

    {
        // here is scope2

        // v1, v2, v3 is valid in scope2
        let v3 = 30_i32;
        let struct4 = Struct4 { prop1: &v1, prop2: &v1, prop3: &v1, prop4: &v3 };

        // v1, v2, v3, struct3 and struct4 is valid here.
    }

    // v1, v2, struct3 is valid, v4 and struct4 is not exist in scope1
}

My questions:

1- What is difference between Struct3 and Struct4, why we need multiple lifetime specifiers? Already all references has to be valid, so why we need to specify multiple lifetimes?

2- Strcut1 has only one property, so why we need to set lifetime specifier? Already there is only one property here, single property can't have multiple lifetime specifier. I think compiler must understand this.

I understood ownership, borrowing, single mutable reference, multiple unmutable references. But I can't able to understand lifetime specifier. I watched lots of videos, read lots of articles but nobody explain the real reason. Please explain me what's real reason of this.

1 Like

This question doesn't really make sense. Whether or not you need multiple lifetime parameters doesn't depend on whether "all references have to be valid" (whatever you mean by that). Instead, it depends on how many different lifetimes you want to allow.

Lifetime parameters are generic parameters, just like type parameters. If you need every reference to be potentially valid for a different lifetime, then you need multiple lifetime parameters. If you want all of them to be valid for the same duration, then you need to declare all of them with the same lifetime parameter.

A parallel with generic type parameters would be:

struct PointA<T> {
    x: T,
    y: T,
}
struct PointB<T, U> {
    x: T,
    y: U,
}

Whether you want the PointA or PointB declaration depends entirely on your use case: do you want to allow different types for x and y or not?

I don't really get what you are asking here. There is a single reference and a single lifetime parameter. Sounds correct to me.

It sounds like you don't understand why lifetime parameters are at all needed. That's because references are only complete types with a lifetime annotation. &T is not a type; it's a type constructor that only becomes a proper type when the lifetime is known, so &'a T is a type.

In a local context (e.g., function arguments and local variables), the compiler can usually infer the lifetime parameter of references, so you can get away with writing &T and the compiler will actually infer a correct lifetime.

But in a global context, e.g. a type declaration, there's no information from which the compiler could infer the lifetimes, just like it couldn't infer the types of struct fields. I.e., neither the following two declarations compile for exactly the same reason:

struct BadType {
    field: _, // no explicit type given
}

struct BadLifetime {
    field: &i32, // no explicit lifetime given
}
10 Likes

I think you didn't understand what I asked. In my example code there are two scopes (scope1 and scope2). We have to send valid variable references to structs, so why we need to set lifetime specifier? Already we can't pass reference a variable which doesn't valid.

You don't need do specify multiple lifetimes for most use-cases, you can. Being able to do one thing multiple ways is a typical thing for general-purpose programming language.

Having only one lifetime is enough for most structs in my experience. There are some instances where multiple declared lifetimes are necessary. This happens when you want to "extract" a field of a struct and don't want lifetime of the reference shortened.

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

fn something<'a>(x: &'a str) -> &'a str { // explicit lifetimes here for clarity
    let y = String::from("other"); // local string
    let s = S {x, y: &*y};
    // do something....
    s.x// return s.x only possible because of separate lifetimes of x and y in S
}
3 Likes

Because you could copy or move struct4 out of its original scope. In C or C++ the compiler doesn’t stop you, which leads to dangling references. But in Rust struct4 has a type that binds it to the scope where the references are valid. And the type is different in every scope because it’s parameterized with different lifetimes. Also, scopes inside a function are a toy example, lifetimes become critically important when you start passing things in an out of functions, or even into different threads.

// ’1
let prop1=1;
let prop2=2;
let s1 = Struct2 { prop1, prop2 };
{
    // ’2
    let prop3 = 3;
    let s2 = Struct2 { prop1, prop3 }
}

Here, s1 and s2 have different types: Struct2<’1, ’1> and Struct2<’1, ’2> respectively, even though you can’t write the actual names of the lifetimes that way. But that’s roughly how it looks to the compiler.

2 Likes

Maybe, we can get syntax like struct X { p: &'_ str } to mean struct X<'a> {p: &'a str}. I would find this nice, but I guess there are bigger issues.

EDIT: thinking about this at this again, this is really tricky to implement in a useful way. If X has other generic arguments, at which position does the lifetime appear? What do multiple _ as lifetimes mean exactly?

The short answer is so that the compiler can "prove what you mean."

These named lifetimes exist so that the programmer can associate the lifetime of references with one another. Either by using the same name (hence the same lifetime, e.g. your Struct4) or with a constraint like 'a: 'b, meaning that 'a is a subtype of 'b (or more commonly described as "'a outlives 'b").

Without a constraint (as in your Struct3), there is no relationship between the lifetimes at all.

If your lifetimes don't have names, you wouldn't be able provide any relationship information to the compiler. And that would make it impossible to express many valid uses of references.

4 Likes

Actually you hit to so important thing which I didn't think before. We can move struct4 to another thread. I must think about that. May be this is main reason for spawn of lifetime sh*t.

You cannot send borrowed data to a spawned thread (unless scoped)

So how can we move/copy struct4 to another scope?

there are three ways to send data:

  • move data into the closure passed to thread::spawn
  • store data into a shared (locked or concurrency-friendly) data structure
  • send data accross std::sync::mpsc channels

all need your data to be 'static. This requires the struct to own the data.

struct Struct4 {
    prop1: i32,
    prop2: i32,
    prop3: i32,
    prop4: i32,
}

Or, if you want to share ownership:

struct Struct4 {
    prop1: Arc<i32>,
    prop2: Arc<i32>,
    prop3: Arc<i32>,
    prop4: Arc<i32>,
}
1 Like

By that logic, you wouldn't have to annotate fields' types, either. The reason why you need lifetime annotations is exactly the same.

Your explanations doesn't explain the situation. I think you didn't understand the reason of lifetime.

You can rest 100% assured that I understand how lifetimes work.

I tried to explain why they are required above. It is you that doesn't understand the answers.

2 Likes

That proves you didn't understand my question. I already didn't understand why lifetime exists. I'm asking that already :slight_smile:

Sorry if someone else has already mentioned this before, but a simple explanation for the lifetime syntax is that they are part of a type.

If you come from other statically-typed languages and you understand that String and Option<String> are different types, then you should know that String and &String are different types as well; the key concept here is that &String is actually sugar syntax for &'a String, and that lifetime specifier is a generic lifetime type parameter. If you understand generics and how to use them with structs, then you know that this is how you declare a struct that receives a generic argument for a given field:

MyStruct<T> {
  id: String,
  data: T
}

And you should also understand that the following syntax doesn't make sense because the compiler won't know where T comes from:

MyStruct {
  id: String,
  data: T
}

Well, just as with generic type parameters, generic lifetime parameters MUST be specified for the compiler to understand your code. The following won't compile:

MyStruct {
  id: String,
  data: &'a String
}

But if you do the same as with the example for T, then it would compile:

MyStruct<'a> {
  id: String,
  data: &'a String
}

The rest of the story about lifetimes in structs has already been covered by other replies. Just wanted to make sure that this part was covered as well.

1 Like

I wonder why every time this question is asked the answers are about how lifetime work, not about why they do exist.

The answer is simple: lifetimes are not, really, part of your program. Not in Rust. Maybe in some other language they would be part of the program but Rust they are not, Existential proof: mrustc doesn't support them, gccrs doesn't support them yet (but they promise to support before release), etc.

Ok. If lifetimes are not part of your programs then why the heck they even exist at all? The answer is not that hard, actually: lifetimes are embedded proof of correctness of your program which reader and, more importantly, compiler, may understand.

This makes Rust a language half-way between something like “normal” programming language and something like Google Wuffs and/or formally verified C.

The differences are two-fold:

  • Lifetimes only guaranteed limited amount of correctness (they wouldn't catch simple type where you write a + b instead of a - b).
  • Lifetimes are compact (in many languages with formal proofs said proofs may be not just larger than actual code, but much larger than actual code) and that means Rust may declare them mandatory, non-optional.

Experience have shown that error messages which compiler may provide if you specify your lifetime annotations right are extremely valuable and people like them. But not everyone and newbies, invariably, pass through the stage of “why do these sigils icons exist and why do they prevent me from creating program freely”?

The answer is: they are not for “you”, they are for “future you”. Sometimes for short-term future you (if compiler notices that your code misbehaves every time you call certain function and you avoid crashes), most of the time for long-term future you (if you keep reference to HashSet element while some other code adds items then for a long time nothing would work, but sooner or latter HashSet would resize itself and you spend days debugging fragile and hard to reproduce heisenbugs).

And if you understand what lifetimes are and why lifetimes exist… you can see why you may want to have different lifetime for different references in the same struct.

Now you can see why sometimes you need different lifetimes in a struct and why sometimes you need the same lifetimes.

Consider the following code:

struct Pair<'a> {
    a: &'a str,
    b: &'a str,
}

fn change<'a>(pair: Pair<'a>) -> Pair<'a> {
    pair
}

Just a pair of references and function that does nothing. But because we used one lifetime sigil it says, basically: “I have no idea how long refences are valid, but you said that I can assume they exist for the sime time”. Now compiler may look on your code and apply that knowledge. Consider this function:

fn foo(x: &str) -> &str {
   let y = String::from("Something");
   let pair = Pair { a: x, b: &y };
   let changed_pair = change(pair);
   changed_pair.a
}

Compiler doesn't know what you wanted to achieve, but it does know that your proof of correctness… it's incorrect. You said that references a and b exist for the same time, but your y object lives and dies in your function yet you try to return a from that function!

How can we fix that? By adding another lifetime and making sure a and b live for the different time and compiler knows that.

Does it mean that it's always better to have two lifetimes? No. For structs it's almost always the best choice, but for functions it's not.

Consider the following modification of change function:

fn change<'a>(pair: Pair<'a, 'a>) -> Pair<'a, 'a> {
    let mut new_pair = pair;
    core::mem::swap(&mut new_pair.a, &mut new_pair.b);
    new_pair
}

This function is correct precisely because we promised to the compiler that references a and b live for the same time… but, of course, now the whole program is incorrect! Because you have swapped references now your foo tried to return reference to a local object which would be destroyed at the end of that function!

So, the summary:

  1. Lifetimes are not, really, parts of your program, rather they are something you tell the compiler.

  2. And what you tell the compiler is, essentially, the proof of correctness.

  3. That story is both for the compiler and for the readers of your code. It should explain how your code behaves.
    Both reader and compiler have to “understand” how your data structures work by just looking of signatures of functions and declaration of data structures.

  4. While reader of your code can understand your code for real the compiler couldn't. But it can verify that your annotations include a valid proof of correcntess.
    You should always keep in mind than compiler is certifiable loon, it's insane… by definition. Simply because it doesn't have any organs which may make it sane. It's all too hard to believe in that because, especially with Rust compiler, error messages are so sane… but they are sane because computer developers are sane, not because compiler is sane! Any compiler for any language is insane… don't forget that.

6 Likes

Lifetime specifiers are needed as part of function interfaces, so that users of a function know what to expect:

fn f<'a, 'b>(a: &'a i32, b: &'b i32) -> Struct1<'a> {
    todo!()
}

fn g<'a, 'b>(a: &'a i32, b: &'b i32) -> Struct1<'b> {
    todo!()
}

Function f is allowed to include a as part of the return value, function g is allowed to include b as part of the return value, and not vice-versa.

If lifetime specifiers for Struct1 were omitted, you wouldn't be able to differentiate between these two different interfaces.

4 Likes

I find that statement rather confusing. Perhaps I am interpreting what you say incorrectly but it sounds wrong to me. Why? Because we declare variables inside scopes. When those scopes end the variables cease to be. They have lifetimes simply by virtue of that.

This is true of every compiled language I have ever used, Ada, Coral, PL/M, Pascal, C, C++. And it is true of Rust.

Even in Rust I get to have lifetimes in my program even without ever using lifetime tick marks or even knowing what they are for. For example:

fn f() {
    let x = 22;
    let xref = &x;
    let yref;
    {
        let y = 23;
        yref = &y;
    }
    println!("{:?}, {:?}", xref, yref)
}

Ooops... "^^ borrowed value does not live long enough" says the compiler.

Those other languages may well let you write such code. They have no notion of lifetimes in their syntax or semantics. They just get you to deal with the resulting lifetime problems at run time, when the thing produces garbage results or crashes.

Rust lets us put names on those lifetimes, which turns out to be very useful.'

By analogy, gravity has always been around, even before Newton came up with some maths to describe it. Similarly programs have always had lifetimes, only now we have a facility to describe them and reason about them. It's that kind of fundamental.

Of course users of language like Python, Javascript etc never (rarely) hit lifetime problems or ever have to think about it. Those languages hide problem by effectively extending lifetimes to infinity by using allocators and garbage collection.

Or did you mean something different....?

3 Likes

Would the full name “lifetimes specifiers” be easier to understand? Sure, lifetimes in your head may be considered part of the program. And they existed before Rust existed.

But lifetimes markup in Rust is somewhere between comments in other languages and code of program. Some compilers just ignore these completely, some may use them to do verification of you program (like doctests hidden in your comments can be used, too), but they don't directly affect the generated code. At least in Rust as it exists today. There are some talks and discussions about maybe making that possible, but AFAIK today Rust can still be compiled correctly if you ignore lifetime specifiers entirely.

Yes, but useful… for what? Like comments lifetime markups couldn't affect how your program works.

But like doctests they can be used by automatic tools to find errors in your programs. Only, like with tests and doctests, they may find not errors in your programs but errors in your markup, too!

I was talking about “lifetime specifiers” as is written in large bold letters at the top of your screen. And hoped no one would misunderstand me because of these bold letters.

I was wrong. Sorry for that.