Lifetime Questions

I have been batteling lifetimes. If I am going to use Rust to its potential I need to come to terms with them.

I get the concept, I see what they are for but the syntax escapes me completely. There is a lot of what look like "magic incantations" to me that serve no purpose. I trust the writers of the rust compiler, the fault is mine.

Reading the chapter in TRPL leaves my with some questions about life times.

(1)

In the following example programme (from the book)....

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {                                                                                                                      
    if x.len() > y.len() {                                                                                                                                               
        x                                                                                                                                                                
    } else {                                                                                                                                                             
        y                                                                                                                                                                
    }                                                                                                                                                                    
}                                                                                                                                                                        
                                                                                                                                                                         
fn main() {                                                                                                                                                              
    let string1 = String::from("long string is long");                                                                                                                   
                                                                                                                                                                         
    {                                                                                                                                                                    
        let string2 = String::from("xyz");                                                                                                                               
        let result = longest(string1.as_str(), string2.as_str());                                                                                                        
        println!("The longest string is {}", result);                                                                                                                    
    }                                                                                                                                                                    
}                                                                                                                                                                        

What is the point of the declaration of the lifetime 'a in angle brackets?
fn longest<'a>... It serves no visible purpose as...

fn longest(x: &'a str, y: &'a str) -> &'a str {                                                                                                                          
    if x.len() > y.len() {                                                                                                                                               
        x                                                                                                                                                                
    } else {                                                                                                                                                             
        y                                                                                                                                                                
    }                                                                                                                                                                    
}                                                                                                                                                                        

...is unambiguous

(2)

Is there ever a case....

impl<'a> ImportantExcerpt<'a> {                                                                                                                                          
    fn level(&self) -> i32 {                                                                                                                                             
        3                                                                                                                                                                
    }                                                                                                                                                                    
}                                                                                                                                                                        

Where there are two distinct life times here? E.g:

impl<'a> ImportantExcerpt<'b> {                                                                                                                                          
    fn level(&self) -> i32 {                                                                                                                                             
        3                                                                                                                                                                
    }                                                                                                                                                                    
}                                                                                                                                                                        

If not, why declare 'a twice?

In fact, why declare it at all? In what circumstance would leaving out the declaration make the syntax ambiguous?

(3)

In this case why are life time declarations needed at all? The declared life time is not used

impl<'a> ImportantExcerpt<'a> {                                                                                                                                          
    fn announce_and_return_part(&self, announcement: &str) -> &str {                                                                                                     
        println!("Attention please: {}", announcement);                                                                                                                  
        self.part                                                                                                                                                        
    }                                                                                                                                                                    
}                                                                                                                                                                        

Just like variables, Rust requires you to declare the names of all your generic parameters before you use them. In your examples, these are the declarations:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { 
          ^^^^
impl<'a> ImportantExcerpt<'a> {
    ^^^^  

They both state that the lifetime 'a within the corresponding statement always refers to the same lifetime, but that might be different from the lifetime 'a used elsewhere in your code. This is more important for type parameters, to distinguish them from concrete types that may have the same name; Rust applies the same rule uniformly for all generic parameters (lifetimes, type parameters, and const generics).

You don't have to declare it at all, this code is valid:

impl ImportantExcerpt<'_> {
    fn level(&self) -> i32 {                                                                                                                                             
        3                                                                                                                                                                
    }
}
1 Like

It is important where the lifetime is declared. These are not the same:

impl<'a> MyStruct<'a> {
    fn my_func(var: &'a str) {
        ...
    }
}
impl<'a> MyStruct<'a> {
    fn my_func<'b>(var: &'b str) {
        ...
    }
}
3 Likes

The feature you want is called in_band_lifetime. Here's the related RFC (note that the RFC calles them argument_lifetimes, but the actual feature is in_band_lifetimes). Here's the tracking issue

2 Likes

'a is a generic parameter and all generic parameters are declared in angle brackets.

Yes, that particular example is unambiguous but there are many situations where it can be ambiguous.

In that example 'a is not declared twice, it's declared once and used once.

For that particular example you can leverage lifetime elision, e.g.:

impl ImportantExcerpt<'_> {                                                                                                                                          
    fn announce_and_return_part(&self, announcement: &str) -> &str {                                                                                                     
        println!("Attention please: {}", announcement);                                                                                                                  
        self.part                                                                                                                                                        
    }                                                                                                                                                                    
} 

However that's actually wrong because it desugars to this:

// I'm assuming this is the structure of ImportantExcerpt
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl ImportantExcerpt<'_> {                                                                                                                                          
    fn announce_and_return_part<'b>(&'b self, announcement: &str) -> &'b str {                                                                                                     
        println!("Attention please: {}", announcement);                                                                                                                  
        self.part                                                                                                                                                        
    }                                                                                                                                                                    
} 

But this is more accurate:

// I'm assuming this is the structure of ImportantExcerpt
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {                                                                                                                                          
    fn announce_and_return_part(&self, announcement: &str) -> &'a str {                                                                                                     
        println!("Attention please: {}", announcement);                                                                                                                  
        self.part                                                                                                                                                        
    }                                                                                                                                                                    
} 

I have mixed feelings on lifetime elision because it's only great if you know exactly how it works but if you're a newbie learning Rust for the first time then it's very confusing. I recommend reading Common Rust Lifetime Misconceptions if you want to speedrun learning all of the common lifetime gotchas and misconceptions in Rust instead of learning it the slow and painful way of doing your own experimentation and trial & error (which is how I had to learn it).

1 Like

That looks helpful. I particularly like the examples where elided and expanded examples are given

I am still incredibly confused by the syntax, the declarations that seem superfluous to me, the repetition.... I will digest this and see where it gets me.

Is there a compiler switch to turn off elision? That would be helpful too.

Also it is a error (?) to declare a life time and not use it. If I could turn that off I think it would help. But I am unsure as I find it all so baffling!

Unfortunately, the compiler needs to know how a lifetime parameter is used to determine which variance it should have.

But if it is not used, why can the compiler ignore it?

Adding a lifeitme to a struct requires edits in about four separate places.

Going from static to temporary (generic on a lifetime) is a major change. The syntactical impact of doing so is a speedbump I suppose, but it's just not something you're likely to do for an established data structure, unless part of a significant refactor already... because the semantic differences (and practical limitations) are rather large. Generally you'll know ahead of time if a struct should be borrowing or owning.

1 Like

Actually it is something I do a lot whilst designing a structure.

Introducing, then deciding against, reconsider and put back a reference in a data structure (the most common thing that makes me need lifetimes) is something I do a lot.

Perhaps you should be spending more time in whiteboard-type design: flowcharts, data models, requirements, constraints... that kind of thing. I tend to agree that "should this struct own or reference" should usually be figured out pretty early and not need to change unless you are making sweeping structural changes to go with it.

1 Like

If you know how the reference you may be adding should behave, you can fake it by using PhantomData as a placeholder until you've decided for sure. You should be careful to use the appropriate flavor to get the behaviour you want.

use core::marker::PhantomData;

// Act like you're borrow something from the start
struct MyStruct<'a> {
    // If you know the specific type, use that instead of ()
    pd: PhantomData<&'a ()>, // covariant
    data: usize,
}

// If you wanted you could do this as an alternative...
struct MyStaticStruct<'a> where 'a: 'static {
    pd: PhantomData<&'a ()>, 
    data: usize,
}

//... but the elision rules are probably going to bite in unexpected ways

Of course, if you decide to add a second generic lifetime, or when you decide to take the lifetime away, you'll have to go adjust things elsewhere... just as you do when adding or removing a function parameter, non-phantom struct field, enum variant, etc.

But yeah, it's not a common way to go about things in Rust. Ownership is such a central concept that there's a huge difference between owning and borrowing data structures.