Rust Lifetime Parameters 'static and 'a

I have never been able to fully understand Rust's lifetime parameters.

Here is the following code:

fn foo_x1() {
    let x: &'static str = "123"; 
    let y: &str = &*x;          // equals to: let y: &'static str = x;
    let zString = String::new();

    let z: &str = &zString;

    // Compile error:
    // `zString` does not live long enough
    // borrowed value does not live long enough
    // let zString = String::new();
    //     ------- binding `zString` declared here
    // let z: &str = &zString;
    //               ^^^^^^^^ borrowed value does not live long enough

    let t: &'static str = z;
}

I have three questions and would appreciate your help. Thank you.

  1. Is the lifetime 'a part of Rust's type system? Given 'a: 'b, are &'a usize and &'b usize the same type, or are they two different types (as covariant parent-child types)? Excluding the case where 'a == 'b.

  2. Are lifetime and liveness scope just different terms for the same concept?

  3. In the code above, is the lifetime of variable x the same as the lifetime of the string literal "123" that x points to? If not, does 'static represent the lifetime of x or the lifetime of the string literal "123"?

I know that everyone has a slightly different understanding of lifetimes, and perhaps the Rust official documentation provides a more authoritative explanation.

Is there an official document about lifetimes and references? I couldn't find an official document on Google.

yes, they are part of the type system.

no, &'a usize and &'b usize are not the same type. shared references are covariant over the lifetime, so if 'a ≤ 'b, then &'a usize ≤ &'b usize, here I use the operator "≤" to denotate a subtyping relation.

not always, to my understanding. for example, you can check out the ghost_cell crate and the corresponding paper.

in this example, they are the same: the type of both the variable x and the literal expression "123" is &'static str.

however, the comment about y is incorrect. when you omit the explicit lifetime (for which only 'static can be specified, you cannot name "concrete" lifetimes in rust syntax) of the type ascription, the type checker will infer some lifetime for you.

the implicit lifetime may be 'static if that's the only valid solution, but it does not need to. the inferred lifetime will correspond to the smallest region that can satisfy the type checker. in this example, conceptually, the region starts at the line where the borrow is created (reborrowed from *x), and it ends where the last use of y, which is the same line. so the type of y is a borrow that lives only in a single line of code.

I strongly recommend you to read this blog post to get a better understanding of the formulation of lifetimes, or, regions:

https://smallcultfollowing.com/babysteps/blog/2018/04/27/an-alias-based-formulation-of-the-borrow-checker/

1 Like

In Rust, the term lifetime (like 'a or 'static) is unfortunately not quite the same thing as the general term of lifetime/liveness/lifecycle used in computer science.

Rust lifetimes are scopes of loans.

How long objects actually live for may be separate from that. Self-contained (not borrowing) types like String can live for as long as they want in whatever scope they want, and that is completely unrelated to Rust's loan lifetimes.

Rust's lifetimes apply only to loans (references or types containing a reference).

Watch out for T: 'static in generics. This doesn't mean that T lives for as long as 'static! Types that don't borrow anything are unaffected by lifetime bounds, and their liveness/lifecycle can be shorter than static! The 'static only applies to types that are borrowing something.

3 Likes

Your statement just gave me a lot of insight: "Rust lifetimes are scopes of loans."

So how should we understand this in the case of let xxx: &'static str = "123";?
Should we split "&'static str" into separate "&" and "'static str" components?

If Rust lifetimes are only used to represent scopes of loans, then we can no longer use lifetimes to describe the actual existence of the variable xxx, and instead, we would have to use variable scope to describe the existence of xxx.

Following your viewpoint, does this mean that Rust doesn't really have the concept of lifetimes in the usual sense?
The term "lifetime" itself is highly misleading. Do I understand this correctly?

I think this is a fair takeaway, though perhaps the main issue is just in calling e. g. 'a the "lifetime of x" when you have some x: &'a T. I. e. differentiating the "lifetime of a variable" from the lifetime parameter denoted in a type of some reference (or similar).

If you differentiate this, then the lifetime parameter of a reference type denotes an lower bound on the scope/time-frame for which the confined reference must be valid, which in turn - if it's a reference pointing to some variable is limited by the actual lifetime of that variable.

The net effect is that when references borrow from a variable, the "lifetime of the reference" can be at most the "lifetime of the variable", though for the former "lifetime of" we mean the scope denoted by the 'a mark in the reference's type whereas the latter "lifetime of" does mean actual lifetime, as far as I understand how you're interpreting this term.

If the reference itself is stored in a variable then speaking of the "lifetime of the reference" can of course be very confusing, because the actual value that is the reference, as well as the variable holding it, each can exist - or be 'life' respectively - for much shorter.

1 Like

References have two generic parameters, the lifetime (here 'static) and the type (here str). You can understand this as borrowing some static str data for 'static (forever).

That is their primary use. There are other uses. I layed some out here, but possibly that's too much of a data dump.

I use the phrase liveness scope, but yes. They are not unrelated as dropping/being destructed conflicts with being borrowed, and types are invalid outside of their lifetime parameters. But Rust lifetimes are not the liveness of values.

It is an unfortunate overlap of terminology. "Duration" or perhaps "region" would have been a better choice.

I don't know what exactly you mean by the usual sense.

There is an analysis of when things may drop/be destructed (and that can involve runtime drop flags to avoid double dropping, etc). Dropping (or potentially dropping) conflicts with being borrowed - but so does being overwritten, being moved, or having a &mut taken. So dropping isn't unique in that sense.

Borrow checking happens at compile time, and lifetimes are erased by runtime too, so borrow checking and Rust lifetimes don't care when a destructor actually, definitely runs during runtime. (Which some may consider to define a "lifetime".)

I agree with this.

Even though there is some relation between the two concepts, I prefer to think of Rust lifetimes as (primarily) borrow durations, and of drops as a kind of implicit use which can conflict with being borrowed (as can other uses) to avoid confusion.

3 Likes

You can think of &'static as borrowing from the executable or leaked heap memory, both of which stay around for the entire duration of the program, so that's the maximum scope.

But for purpose of reasoning about borrow checking, I suggest ignoring the existence of 'static. It's a special case that is an exception to almost anything that can be said about the borrow checker. It's almost a separate language feature on its own.

Should we split "&'static str"

Lifetimes are always attached to &, so it's &'static (& loan borrowed for scope/duration described by static).
The borrow checker checks for you whether the str that you're getting a reference to is compatible with borrowing it as &'static or some other &'a or an unnamed lifetime, but the lifetime is always a property of the reference (the loan you take), not the data behind the reference. For example you very often create and end temporary loans of data that will live longer than the loan, but you're only allowed to use it for the duration of your loan, regardless of what happens later to it.

fn main() {
    let forever_string: &'static mut str = Box::leak(Box::from("forever_string"));

    temp(forever_string);
    // short_loan stops existing here, but forever_string exists
    drop(forever_string);
    // forever_string stops existing here, 
    // but the data of the `Box` still exists, 
    // just not accessible any more
}

fn temp(short_loan: &str) {
    let even_shorter_loan: &str = &short_loan[8..];
    println!("{even_shorter_loan}");
    
    // let _: &'static str = short_loan; // not 'static for *this* function
    // even_shorter_loan stops existing here
}
1 Like

That's sound advice, but…

it means precisely that. If you write T : 'static then means precisely and exactly that.

The key insight is here:

If you put lifetime (the runtime concept) in type (static, compile-time concept) then you are half-way on the road to the dependent typing.

Any type that's T: 'static is a single, concrete type, be it i32 or f64 or even String: variables of all these types are all identical and can be moved willy-nilly.

But any type that's not T: 'static is “dynamic” type! Since type &'a str doesn't exist!

Instead your program includes infinite number of such types.

One may even imagine some kind of research language where such types actually exist at runtime, are created “on the fly” and can be probed and processed using runtime reflection.

But Rust doesn't want that! Instead it postulates that we have to disallow any and all constructs where it's not possible to generate one, single, sequence of machine instructions that process all these different types simultaneously.

The closest analogue is type erasure in Java.

What's misleading are attempts to reduce number of lifetimes in discussions about your program and they cry that everything is, suddenly, incorrect because different things all have the same lifetime… but the confusion here is simply the result of that oversimplification!

In reality lifetimes are not too complicated if you just accept that everything in Rust can have lifetime attached: types, variables, variables contents, loans, etc.

'static str doesn't make any sense. But str: 'static makes sense (and is, in fact, needed to be able to say &'static str).

Basically: any entity with nonfixed lifetime may only exist as long as another entity (that said entity refers) exist… but it can live for a shorter time!

Consider somewhat more complicated code:

fn foo<'a>(x: &'a i32) -> &'a i32{
     let y;
     {
         // println!("{y}") – incorrect because lifetime of `y` and lifetime 'b are different.
         let z = &x;
         y = bar(z);
     }
     return y;
}
fn bar<'a, 'b>(m: &'a &'b i32) -> &'b i32 {
     return *m;
}

Here you can see that lifetime of variable (x or y) is different from lifetime of value that's in that in that variable and it's different from lifetime of type and two named lifetimes ('a and 'b are different).

Normally we talk about lifetimes of loans, because we only care about loans, but in reality everything has a lifetime. And if you don't try to postulate that variable (== place in memory) should live for the same time as data in that variable (it may live for a longer or shorter time than place in memory!) and then understand that type of reference carriers intersection of two (reference is only valid as both place where it points to and it's content are both valid), but then reference is a variable, too and that means that place where you put that reference have a lifetime, too.

When we postulated that “all lifetime should be erased after compilation” we gave themselves a carte blanche to put as many distinct lifetimes as needed… and thus everything got a lifetime.

So it's not that “variables don't have lifetimes” in Rust, but more of “not just variables have lifetimes”… and since to actually use variable for anything you have to ensure that both variable and value in it are “alive”… we usually talk about lifetime of loans.

These lifetimes are the most useful… but that doesn't mean others don't exist!

1 Like

The key clarification is that T: 'static doesn't mean values of type T are static values or otherwise never drop. That is a common misunderstanding when learning the language, and worth pointing out to the OP.

Outlives bounds on types are assertions about the type -- any lifetime in the fully resolved type on the left must outlive the lifetime on the right. In the typical case, you can interpret T: 'static to mean "T contains no temporary borrows" (and U: 'a to mean "U contains no borrows shorter than 'a").

(But the outlives relation is defined syntactically, and a lifetime parameter doesn't always mean the struct contains a borrow, etc. So be aware that the interpretation is an approximation of the actual type-system level meaning.)

4 Likes

"T: 'static" means "lives for static" or "outlives 'static" only because that's the wording Rust uses to describe such lifetime bounds, and Rust can call them whatever it wants.

But to me that's an unhelpful jargon, which uses lives in a sloppy way that's very Rust-specific, and is setting up wrong expectations for anyone who interprets "lives" in a broader CS meaning of a span between creation and destruction of an object.

This terminology creates paradoxes like this:

fn lives_for_static_but_doesnt_live_long_enough<T: 'static>(t: T) { 
    let r: &'static T = &t;
}

I have a T that is said to "live for a 'static lifetime", but I can't take a &'static reference to it, because T doesn't live as long as 'static!

A more explicit case where Rust's typesystem "lives" doesn't match outside-of-Rust meaning of object lifetimes:

fn is_that_static<T: 'static>(v: T) -> T { v }

fn main() {
    let longer_lived_reference = &String::new();
    {
        let shorter_lived = String::new();

        let shorter_lived = is_that_static(shorter_lived);
        // is_that_static(longer_lived_reference); // does not live long enough

        // dropped in a scope smaller than 'static!
        drop(shorter_lived); 
    }
    drop(longer_lived_reference);
}

I can have a type T equal to String, which satisfies the T: 'static bound ("lives for 'static"), but is created and destroyed (lives) in a scope smaller than 'static! And I have another instance of String that lives in a larger scope than my T: 'static String, for longer than the T: 'static String, and yet it "lives" for less than T: 'static, despite living for longer than the another instance living for T: 'static. Having to describe object liftimes using different meanings of "lives" is like the Who sketch.

3 Likes

How? Types that include non-trivial references (that is: non-'static ones) are ephemeral: they are created on the fly and they disappear when place that they reference disappear.

No, T lives as long as 'static, but t doesn't live as long. To successfully take the reference all components much match: lifetime of the variable type and lifetime of variable itself, too!

Where does type is created in that example??? You are creating variables here, sure, but you don't reference them in is_this_static thus there are no new types created!

The type of longer_lived_reference is ephemeral in your example, it's created and destroyed, but it doesn't outlive type String! Indeed, if you would make your main return String and write return shorter_lived – everything would work… precisely because type of shorter_lived is 'static.

But the same thing wouldn't work with longer_lived_reference because that type is not 'static. It refers something in your function thus it couldn't outlive it. And if type couldn't outlive function then data of that type also couldn't outlive your function.

As I have said: you are conflating lifetimes of two different entities (type and variable of said type) then complain then it doesn't work.

Well, of course it doesn't work! But the solution is simple: don't conflate lifetime of a type and lifetime of variable of that type.

These two lifetimes are absolutely identical in nature but they are applied to two different things!

Sure, but that's not because there are anything wrong with Rust's terminology but because you insist on conflating lifetime of type and variable of that type. That's just simply wrong, simply don't do that.

But if you would accept that types have lifetime, variables have lifetime (different from lifetimes for types!), data in these variables have lifetime (again: different from the lifetime of type and lifetime of “place”, in-memory location!) then there are no contraditions or confusions at all: new types are created on “as needed” basis, when you take a loan they contain an intersection of place where data lives and lifetime of type that lives in that place. If one or the other is short-lived – then that information is “stored” in type.

Of course it's “stored” in the exact same sense as type String is stored in the type of HashSet<String> in Java, compile-time only, it's not reflected in the generated code… but so what?

If we would change your example just a tiny bit:

fn is_that_static<T: 'static>(v: T) -> T { v }

pub fn not_main() -> impl Any {
    let longer_lived_reference = &String::new();
    {
        let shorter_lived = String::new();

        let shorter_lived = is_that_static(shorter_lived);
        // is_that_static(longer_lived_reference); // does not live long enough

        return shorter_lived;
    }
    //return longer_lived_reference;
}

This works. Because type of shorter_lived is 'static. But with if we swap return then compiler complains. Why does it complain? What's the issue if, as you say, types don't have lifetime?

That doesn't make the term any less ambiguous in communication. You're relying on the other person already understanding a very subtle language-theoretic distinction, which isn't useful for anything practical in Rust, apart from understanding this justification. Your explanation adds even more phrases like "creating a type" that must be understood in the academic way you mean them, creating another possible point of confusion for people who aren't already familiar with PLT (e.g. in languages with dynamic runtimes, creation of a type is a thing that can happen literally at run-time).

So I still think it's a poor phrasing. You can give someone a lecture on advanced type theory with its context-specific definitions of its own take on the terminology, or you can say that T: 'static just doesn't apply to types without references.

What “subtle language-theoretic distinction”? Isn't the whole Rust built around similar distinction between value and place? The move semantic, liveness analysis and most “new” properties of Rust are related to the fact that Rust separates things that are the same in most other languages: the ability to name variable, and the ability to use variable.

Doing one more such step doesn't sound too complicated.

The problem is that this explanation wouldn't help you to use different kinds of restrictions where T: 'a and 'a is not static. And how do you plan to explan how precise capturing works with your approach?

I don't think one may sidestep the important part of the language by just inventing better phrasing.

Lifetimes are encoded in types in Rust and yet erased after compilation… this creates all kinds of questions.

E.g. here:

fn main() {
    let mut r: &mut i32;
    {
        let mut x: i32 = 42;
        {
            let mut y: i32 = 57;
            r = &mut y;
            *r += 1;
            println!("y: {y}");
        }
        // Does anything exist in `r` at this point?
        r = &mut x;
        *r += 1;
        println!("x: {x}");
    }
}

How do you propose to explain such example without “advanced type theory”?