Rust Learning: Lifetime
Disclaimer
I’m in the process of learning and figuring out my confusions, there might be mistakes. What I’m writing here is more of me trying to figure things out and some of my current understandings.
tldr
- Can be used in functions, in structs, in traits
- lifetime subtyping
'long:'shortmeans'longoutlives'short, also'longis a subtype of'short- Three syntaxes
- Right after
&means annotating the lifetime of the reference - As generic lifetime parameters
- In trait bounds
- Right after
'static- Right after
&means this reference needs to be available for the processes lifetime T: 'staticmeans- For reference fields in
T, the lifetime has to be valid for the entire program - Or
Towns the fields
- For reference fields in
F: 'staticwhereFis closure meansFshould own the captured data (moveclosure)- Or
Fonly capture'staticdata.
- Right after
- Type outlives lifetime
T: 'a- For reference fields in
T, the lifetime has to outlive'a
- For reference fields in
Overview
I recently came across learning Rust (again, like the 100th time). I thought maybe if I start writing things down, it helps solidifying my understanding a bit.
This post is about some of my confusions and thoughts that occurred when I was reading the Rust book.
My first confusion
In Chapter 10 Section 3, the example function was trying to explain how to write a function that returns the longer string without taking ownership.
I’ve seen this many times, but this time I wanted to dig a bit deeper.
The book says:
The function signature now tells Rust that for some lifetime ‘a, the function takes two parameters, both of which are string slices that live at least as long as lifetime ‘a. The function signature also tells Rust that the string slice returned from the function will live at least as long as lifetime ‘a.
In practice, it means that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the values referred to by the function arguments. These relationships are what we want Rust to use when analyzing this code.
The part that got me thinking was both of which are string slices that live at least as long as lifetime ‘a.
Looking at the function signature, I couldn’t see where it expressed at least as long as. Say if I have a function that takes two ints, I know I need exactly two ints.
Again, long conversations with Gemini, the reasoning seems to be called lifetime subtyping. And it’s similar to the OOP concept that I am familiar with.
In the following code, we are passing in a Cat to a function that takes an Animal, and it seems normal.
|
|
This is polymorphism 101: a Cat inherits from Animal.
Wherever I need an Animal, I could pass in a Cat because Cat inherits from Animal; in other words, Cat is an Animal.
What I didn’t know was, there is another “in other words”: Cat is a subtype of Animal.
With the detour above out of the way, let’s have a look at the function again:
There are 4 'as, they are not all the same, the first one longest<'a> is where we are declaring a generic type 'a.
The later 3 are using 'a declared by us
If you look at lifetime 'a as a type, then combine what we said about polymorphism and subtypes, things start to make sense.
The parameter x: &'a str is saying if the passed in argument’s lifetime “satisfies” 'a then it’s good.
“satisfying” here in lifetime means the lifetime passed in can not be gone before 'a.
In other words, the lifetime passed in has to live at least as long as 'a.
So say the lifetime of the argument is 'b, what we are saying above is: 'b has to be at least as long as 'a.
I happen to know that there is a term in Rust called outlive.
If we have two lifetimes 'long and 'short, then the syntax 'long:'short in trait bounds means 'long outlives 'short.
We’ll come back to this when we talk about lifetime in trait bounds.
So now it is clear to me why did the book say both of which are string slices that live at least as long as lifetime ‘a. It is because the lifetime parameter we declared in our function signature is like a “Base Class”, you could pass in an argument with a lifetime which has a longer lifetime and it would satisfy the parameter.
The other thing is when you pass in two different lifetimes to longest, rust would find out the overlap and bind it to 'a
Lifetime on structs
With above it is clear to me that lifetimes in Rust are special generic types, and fn longest<'a>(x: &'a str, y: &'a str) -> &'a str is a generic function.
Naturally after a generic function you would think about a generic class.
This is where I got confused yet another time by the syntax. But to be fair I think the confusion comes from the generic syntax not from lifetimes(they are all generics though)
So far things seems pretty normal, struct User<'a> defines a struct which has a generic type parameter (in this case it’s a lifetime) 'a.
And the lifetime is used to say that the string slice you refer to that represents the name needs a lifetime, and that lifetime needs to outlive 'a
I started to feel confusing when I wanted to create a constructor.
In Rust methods are defined outside of the struct using impl.
The following is what I did at first try.
The compile error is “undeclared lifetime”, and the fix is to add <'a> after impl
error[E0261]: use of undeclared lifetime name `'a`
--> src/main.rs:5:11
|
5 | impl User<'a> {
| ^^ undeclared lifetime
|
help: consider introducing lifetime `'a` here
|
5 | impl<'a> User<'a> {
| ++++
This got me thinking, why do I need to have <'a> after impl, isn’t User<'a> enough to say that my type is taking a generic lifetime parameter?
impl<'a> is where 'a is declared as a new generic parameter. Without it, Rust sees 'a in impl User<'a> as an undeclared name — the same way using a variable before declaring it would be an error.
So now with the syntax issue resolved lifetime on structs are clear. Let’s recap
- When defining a struct you can take in lifetimes
- lifetimes are special generic lifetime parameters hence they follow the same generic syntax
- The generic lifetime parameter is instantiated when we create an instance, it binds to the lifetime it get passed to (So our struct “passes through” the lifetime to it’s own fields)
- The lifetime took in could annotate reference fields in the struct (which the struct does not own)
- Rust would make sure that the lifetime we took in outlives the instance of our class
See Gemini generated example below (well Gemini just took it from here)
|
|
This struct has the single field part that holds a string slice, which is a reference. As with generic data types, we declare the name of the generic lifetime parameter inside angle brackets after the name of the struct so that we can use the lifetime parameter in the body of the struct definition. This annotation means an instance of ImportantExcerpt can’t outlive the reference it holds in its part field.
When we created an instance of ImportantExcerpt, it also took in a lifetime parameter, the lifetime i took in is the lifetime of novel because ImportantExcerpt took in first_sentence which is a &str which refers to novel.
The lifetime tracks where the data lives(novel), not what variable you used to access it(first_sentence).
So as long as novel outlives i Rust would not complain.
Side note: Gemini actually hallucinated pretty hard here, in one version it keeps saying that 'a is the lifetime of i. I’m actually considering subscribing to Claude
Lifetime on Traits
Self Review Note:
After learning lifetimes on structs, I was looking at lifetime on Traits, the following is an example Gemini gave me. It seems like the trait bound
T: 'ais redundant and is implied by Rust.But let’s go through the process together
The Lifetime Bound
|
|
And here T outlive 'a means if T contains any references, those references need to outlive 'a (those references needs to live longer than 'a)
Side topic: I learned that in rust different impl blocks can have different trait bounds, also the struct definition can have trait bounds. Seems like the idiomatic way is to keep the struct clean for bounds and have the bounds on the impl blocks
When staring at the code snippet above I just feel like there is something still not making sense.
Logger takes in a T and references it borrowed_data: &'a T, 'a is the lifetime that is passed in, T is also the type that is passed in, aren’t they the same?
In the above T is String, and 'a' is the lifetime of novel, feels like they are the same thing to some extent?
Claude says
So two more questions here
- Why/how does it imply
T: 'a? - In what cases do I actually need to put
T: 'ain the trait bound?
For (1), the reason why rust can imply T:'a from &'a T is because
&'a Tmeans a reference toTfor a duration of'a- Which means
Tneeds to live at least as long as'a - Which means
Toutlives'a - Which is
T: 'a
For (2) I don’t have a good answer now, but the most common case seems to be traits slapping on the 'static lifetime
The static Lifetime Bound
Here is an example of having 'static in a trait bound.
Having 'static here on T means if T has any reference fields, they have to outlive 'static (which means they have to live as long as the program), or T has to own the fields.
The 'static on F is saying F should own the captured data (move closure) or only capture 'static data
Summary
The deeper I dig the more questions I have. I did find a few links that seems worth checking out
I did have a simliar feeling of leaning C++ again, where you just keep having questions over questions over questions and at some point you don’t remember why you are figuring these things out. And after you think you are crystal clear, how are you going to apply this in your own code? I’m still not sure what is the answer of that. Maybe I need to write a bit more Rust to internalize these concepts?