How to understand this doc description

"Lifetime annotations don’t change how long any of the references live. Rather, they describe the relationships of the lifetimes of multiple references to each other without affecting the lifetimes. "

How to understand this description, I think it is ambiguous. Is there a friend who can help me?

Does it say that the life cycle cannot be dynamically changed while the program is running? Or describe other meanings, I think this official website text should be more clear without ambiguity to express clearly

Think of lifetime annotations as a way to document how long you think different things will be alive for, and possibly things like "I think reference &'a Foo will live longer than &'b Bar" (which can help if Bar wants to borrow something from the Foo).

The compiler then takes that documentation and uses some rules to check they make sense.

At no point does the compiler move anything around to suit the annotations, it's purely a compile-time check.

5 Likes

Thank you for your reply, but I am confused about the word change, that is, I can get a different life cycle by changing the return value of the function, isn't it changing the life cycle of the variable? I think what you're saying is that you can't dynamically change the lifetime of a variable during runtime, not that you can't determine the lifetime of this variable when I define the lifetime. I think this sentence is ambiguous, so I bring it up. Is there something wrong with my understanding? Your continued advice will be greatly appreciated

fn main() {
    // let ret_shot_out:&str;
    let ret_long_out:&str;
    {
        // let arg=String::from("hello");
        // let ret=getShortLifeRef(&arg);
        // println!("{:?}",arg);
        // println!("{:?}",ret);
        // ret_shot_out=ret;
        //================================
        let arg="hello";
        let ret=getLongLifeRef(&arg);
        println!("{:?}",arg);
        println!("{:?}",ret);
        ret_long_out=ret;
    }
    //arg and ret all died in here,panic here
    // println!("{:?}",ret_shot_out);
    println!("{:?}",ret_long_out);
}

fn getShortLifeRef<'a>( arg:&'a str )->&'a str{
    return arg
}

fn getLongLifeRef(arg:&'static str)->&'static str{
    return arg
}

Just so everyone knows, the text is from this chapter in the Book. I don't really like the Book's presentation, since it conflates the liveness scopes of values with Rust lifetimes (those '_ things), and conflates lifetimes with scopes, without pointing out that these are coarse approximations to give you the general idea. (This is the section where the quote comes from. I don't actually know what the quote from the book was trying to convey either.)

So one thing I want to highlight for you is that Rust lifetimes (those '_ things) are not the same as the liveness scopes of values. The Book (and to be fair, most other documentation or other discussions about Rust) doesn't take care to distinguish the two, but I will.

The main connection between the two is that when the scope of a value ends -- e.g. when a value goes out of a scope -- is a use of the value. The borrow checker looks for uses of values that conflict with outstanding borrows. The compiler uses lifetimes ('_) as part of the analysis of which borrows are outstanding.

The compiler does not use lifetimes to decide the liveness scopes of values. This may be what the quote was trying to convey.

Lifetimes ('_) don't exist at run-time at all.

I'm not sure exactly what you mean by "life cycle [of the variable]".

If you change the type of the return value, for example by changing a lifetime ('_), you do change the meaning of the function signature. The function signature defines an API contract that callers must fulfill to call the function, and that the function body must also fulfill in order to compile. You can't call getLongLifeRef(&arg) because you'll get a borrow check error at the call site if you try to create &'static str from &arg. And if you made this change:

//                vv       vv
fn getLongLifeRef<'a>(arg:&'a str)->&'static str{
    return arg
}

The function body would get an error because 'a isn't known to be 'static.

More generally, annotation can definitely change the lifetime ('_) of a reference in some sense. Namely, you can force the type of a reference to have a specific lifetime ('_).

fn example<'a>() {
    // This fails to compile; removing either annotation compiles
    let x: &'a str = "some str";
    let y: &'static str = x;
}

(Usually you don't need or want such annotations inside function bodies.)

But if a program compiles before and after you change some lifetime annotations (but nothing else), the liveness scopes of values -- for example, where a String gets dropped -- does not change.

Maybe this last point is what the quote meant.


Just after the quote, they mention functions, even though they're not in the "function signature" section yet. So let's consider particularly the context of functions which are generic over lifetimes.

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

Lifetimes introduced in the signature of a function have an implicit property: they are always at least just longer than the function body, so that they are valid everywhere within the function. This is why you can never borrow a local variable for a lifetime introduced in the signature: every local variable is moved or goes out of scope by the end of the function, and that's incompatible with being borrowed.

The generic lifetimes are also chosen by the caller of the function. Because the caller chooses the lifetimes, the only other things the body of the function can assume about the lifetime come from the annotation and lifetime bounds in the signature. In the signature above, the annotations mean that the lifetimes of the two input types, and that of the output lifetime, are all the same. If you gave the input lifetimes different names, the function body could no longer assume they were the same.

But no matter what the bounds and annotations are, if the lifetimes are introduced by the function,[1] they still must be longer than the function body, and they are still chosen by the caller. The exact annotations don't change that.

Maybe that's what the quote meant.

Anyway, being only able to assume things about the lifetimes which are stated in the function signature is similar to how type generics work:

// If you remove `T: Clone`, this won't compile, because the caller
// chooses `T` and you can't assume it implements `Clone` without
// making the requirement explicit.
fn my_clone<T: Clone>(t: &T) -> T {
    t.clone()
}

// This doesn't work because the types don't match.
// The caller chooses both types and you can't assume they're the same.
// fn my_push<T, U>(vec: &mut Vec<T>, item: U) {
//     vec.push(item);
// }

// Here we've made them the same by giving them the same "type name" `T`.
fn my_push<T>(vec: &mut Vec<T>, item: T) {
    vec.push(item);
}

The longest function requires the input lifetimes to be the same, similar to how my_push requires the type T to be the same in vec: Vec<T> and item: T.


  1. This doesn't include higher-ranked trait bounds. You don't know what those are yet, but don't worry about it for now. I'm just adding this note so you don't call me a liar when you learn about higher-ranked trait bounds in the future. ↩︎

5 Likes

You understand my meaning, thank you very much, in fact, I mean that the expression of the text of the official website makes me very confused, I do not understand what the meaning of this sentence is, if according to my understanding, I may understand: no matter how I mark the life cycle, I can not change the scope of the value by marking! That's how I understand it, and I'm sure a lot of the documentation doesn't explain it too much, but it's really easy to disagree. In fact, as in your example, we can change the scope of a value in such a way, isn't that changing the life cycle of the value? I think each value has a scope at runtime, and this scope is when the program runs and decides when to discard it and empty the associated memory. You can say the life cycle of the value, but it is more accurate to say the life cycle of the variable, not the value! Because each value or data can have multiple references, and each reference has a different lifetime.

fn example<'a>() {
    // This fails to compile; removing either annotation compiles
    let x: &'a str = "some str";
    let y: &'static str = x;
}

This is correct. No matter how you mark the lifetime notation it doesn't change any runtime behavior. Values are allocated when it's initialized and dropped/deallocated when its scope ends. The only effect those lifetime markers can generate is to throw compile error. And compile errors can't change runtime behavior - if it fails to compile, it can't run and there's no run-time.

7 Likes

The example doesn't change when variables go out of scope. It just uses annotations to force a borrow check error -- it creates lifetime constraints that cannot be satisfied.[1]

When a particular value is actually destructed is a runtime property. My best guess is that this is what you mean by "life cycle", but I'm not sure. Anyway, the runtime properties of a program that doesn't compile aren't really defined.

But lifetimes don't effect the runtime semantics of compiling programs, including when values are destructed.

I don't understand what you mean. But here's a single variable that holds multiple values which get destructed (along with some related examples).


  1. The idea was to show that annotations do change the lifetimes of the types of references, so the quote must not be talking about that (or is just plain wrong). ↩︎

2 Likes

I mean, I can obviously determine the length of a citation by manually annotating each citation with a lifetime, so why does the website say it can't be changed?

My best guess is still that they were talking about liveness scopes or the like...

...and not the Rust lifetimes ('_) which are part of the type of references, which clearly do get changed.

But it's still a guess. I agree it's not clear how to interpret the quote.


I don't know if it's what you meant or not, but note that the lifetime of a reference doesn't change when the reference itself goes out of scope. (And that the lifetime can be longer than the scope.)

References have no-op destructors, or don't have destructors if you prefer, but they still do go out of scope like the playground demonstrates. It's possible this is what the quote was talking about, but I doubt it -- you pretty much never care when a references goes out of scope unless you take a reference to a reference (which is a rare need outside of demonstrating borrow checker behavior).

1 Like

My opinion is that many explanations or guides of lifetimes try to be somewhat formal or exact, but without the rigor of precisely defined mathematical terms to back it up, so people end up with incomplete mental models that cause more confusion than understanding. Making a correct but also useful borrow checker is an incredibly complicated subject but unless you are trying to write rustc (the Rust compiler), I don't think any formal theory is needed to be productive writing other things using Rust. For a block of code a few lines long, as is the case with most toy examples, people are intuitively able to evaluate whether it does a use-after-free or not even if they cannot explain what the borrow checker is doing to verify that. As an analogy, imagine teaching Hello World by first explaining LL parsers. If the goal is to understand how the language is used, trying to understand how it is parsed is a bit of a distraction. Most people can learn the syntax and grammar of a language well enough to use it perfectly fine without knowing anything about what a context free grammar is.

The mental model I use to understand lifetime annotations is simple yet works well enough for me - I just imagine a chain of dependencies that starts with a value's owner and extends through all the borrows and reborrows up to whatever reference I am working with. Within the scope of a single function, the borrow checker is mostly smart enough to figure things out on its own without needing lifetime annotations, but the chain breaks when crossing function boundaries or storing references in arbitrary structs. In those cases lifetime annotations serve as hints telling the compiler how to connect the chains across those boundaries. For example if you write a function that takes two references and returns one, the compiler needs you to tell it which input reference chain to attach the output reference to.

So to get back to the original question,

At the end of the day, the borrow checker is just trying to prove that your code does not contain use-after-free (and similar issues), and for the most part it does not need your input beyond the annotations mentioned above. You cannot lie to the borrow checker by claiming a reference lives longer than the chain it is attached to. That's what I would say it means.

With this mindset let's take a look at some of the code samples posted:

fn getShortLifeRef<'a>( arg:&'a str )->&'a str{
    return arg
}

This function simply returns its input as its output, and the lifetime annotations make that very explicit. Effectively this function does nothing.

fn getLongLifeRef(arg:&'static str)->&'static str{
    return arg
}

This function is just like the one above except restricted to only &'static input.

 // let ret_shot_out:&str;
    let ret_long_out:&str;
    {
        // let arg=String::from("hello");
        // let ret=getShortLifeRef(&arg);
        // println!("{:?}",arg);
        // println!("{:?}",ret);
        // ret_shot_out=ret;
        //================================
        let arg="hello";
        let ret=getLongLifeRef(&arg);
        println!("{:?}",arg);
        println!("{:?}",ret);
        ret_long_out=ret;
    }
    //arg and ret all died in here,panic here
    // println!("{:?}",ret_shot_out);
    println!("{:?}",ret_long_out);

The commented out lines contain a very obvious use-after-free: ret_shot_out is assigned a reference whose dependency chain traces back to a value local to the block, so it cannot be used after the block because that value has gone away. If you are confused about why ret_long_out works, that is because the string literal "hello" is actually a static so it outlives the block, which I presume is documented somewhere. Once you learn that bit about string literals being static, all of this should be understandable on an intuitive level without needing to formalize anything.

4 Likes

Thank you very much for your sincere explanation, I learned a lot from it. However, as for this sentence“ Lifetime annotations don’t change how long any of the references live.”, I personally think it is not rigorous enough or even misleading and wrong. I hope the officials can correct this sentence and give enough examples to explain it, instead of being vague. In my country, many people like me will easily misunderstand the meaning of this sentence, really, I lie to you!
In fact, I am fully aware of the use and meaning of the life cycle, and I am Posting this to get the official to correct this sentence, because in the 3 + years that I have been learning rust, I have been quite misunderstood about this sentence, and I am only now understanding the true meaning of this sentence, which is not what it seems to be on the face of it, and often people misunderstand its true meaning.

1 Like

Can you explain how do you understand that sentence and what exactly is wrong with it?

That sentence talks about the following thought experiment: what if you take Rust program, remove any and all lifetime annotations and ask someone to make it correct. May this give us a program which would work differently from what we had in the beginning? And the answer is: no, that's not possible[1].

That's what that phrase tried to convey: lifetime markup doesn't prescribe anything, it describes how the rest of the code works and gives some information about core structure to both the reader of the code and to the compiler without affecting the code generation at all[2]!

I hope the officials can correct this sentence and give enough examples to explain it, instead of being vague.

What exactly is vague in it and what exactly needs to be explained? You can create a compiler that would just ignore lifetime markup and it would still compile correct Rust program to the exact same machine code! In fact that what mrustc actually does!

It's as simple as that: lifetime markup, added to the program which is already correct, would make your program compile, but if your program is incorrect you couldn't use lifetime markups to change it's meaning! First you need to make program correct and then you may adjust lifetime markup to convince compiler to accept it.

You couldn't return value of the function by changing lifetime markup. E.g. the common mistake of the beginners is to [attempt to] return the reference to the local variable. This is incorrect code and it's impossible to add or change some lifetime sigils to make such program correct.

You need to:

  1. Change your program to make sure that references stay valid and never point to the object that is deleted.
  2. Change the lifetime markup to describe the solution that you have picked to fix the bug in your program.

It's easy to disagree but not easy to present a counter-example. In fact most Rust programmers would never hit the situation where such counterexample is even possible.

Yes, but you are not doing it by changing markup. First you change the code to ensure that it would work different, that it would work correctly, then you change lifetime markup to match. Change of the lifetime markup alone wouldn't do anything.

Think Ada2005 and SPARK2005. SPARK is, essentially, a correctness verifies, static program analyzer which verifies the correctness of the SPARK2005 programs, but SPARK2005 programs are also Ada95 programs, they just have comments of special form which are ignored by actual compiler, but used by SPARK2005 to verify correctness of your program.

Lifetime sigils in Rust are like that: they are comments which are designed to be read by programmer and by separate verifier tool that ensures that your program is correct. Actual compiler may just ignore them, it wouldn't change anything.

That is what this sentence tries to convey. Nothing more, nothing less. And I don't see any ambiguity in it: it says precisely what it wants/needs to say.


  1. It's not 100% correct assertion, just 99.9% correct assertion, there are are certain very tricky HRBT related corner cases where that's not true, but they are so rare that I have never seen that happen in practice, only ever saw that in artificial examples. ↩︎

  2. Again: except for some tricky corner HRBT cases. ↩︎

4 Likes

No. The lifetime of a value is determined by the existence and scope of bindings that hold that value. You can't change the lifetime of any value by doing anything but move its declaration into a shorter or longer scope, or explicitly move/drop it.

Lifetime annotations are generic parameters that allow you to work with references of various lifetimes and relate the lifetimes in generic code. If you declare a variable in a scope, it will live until the end of the scope (or until it's explicitly dropped, whichever comes first), full stop. No amount of lifetime annotations change that simple fact.

1 Like

Simply looking at this sentence in the documentation, I certainly wouldn't look at it from the perspective of time or from the perspective of the program, and I still think it's ambiguous.
Here's my example of why this sentence is inappropriate:

struct Interface<'a> {
    manager: &'a mut Manager<'a>
}

impl<'a> Interface<'a> {
    pub fn noop(self) {
        println!("interface consumed");
    }
}

struct Manager<'a> {
    text: &'a str
}

struct List<'a> {
    manager: Manager<'a>,
}

impl<'a> List<'a> {
    pub fn get_interface(&'a mut self) -> Interface {
        Interface {
            manager: &mut self.manager
        }
    }
}

fn main() {
    let mut list = List {
        manager: Manager {
            text: "hello"
        }
    };

    list.get_interface().noop();

    println!("Interface should be dropped here and the borrow released");

    // 下面的调用会失败,因为同时有不可变/可变借用
    // 但是Interface在之前调用完成后就应该被释放了
    use_list(&list);
}

fn use_list(list: &List) {
    println!("{}", list.manager.text);
}

Run it and report an error:

error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable // `list` can't be borrowed because it is already borrowed as mutable
  --> src/main.rs:40:14
   |
34 |     list.get_interface().noop();
   |     ---- mutable borrow occurs here
...
40 |     use_list(&list);
   |              ^^^^^
   |              |
   |              immutable borrow occurs here
   |              mutable borrow later used here

This code doesn't look too complicated, but it's actually quite difficult. First of all, intuitively, the mutable reference borrowed by list.get_interface() is supposed to be returned at the end of this line of code, but why does it persist after use_list(&list)?

This is because there is a problem with the lifetime we declared in the get_interface method. The lifetime of the method's argument is 'a, and the lifetime of List is also 'a, which means that the method will live at least as long as List, and going back to the main function, list can live until the end of the main function, so the variable reference borrowed by list.get_interface() will be returned at the end of this line of code. interface() will also live until the end of the main function, during which time it can no longer borrow.

To solve this problem, we need to give the argument to the get_interface method a different lifecycle than List<'a>, 'b,' and end up with the following code:

struct Interface<'b, 'a: 'b> {
    manager: &'b mut Manager<'a>
}

impl<'b, 'a: 'b> Interface<'b, 'a> {
    pub fn noop(self) {
        println!("interface consumed");
    }
}

struct Manager<'a> {
    text: &'a str
}

struct List<'a> {
    manager: Manager<'a>,
}

impl<'a> List<'a> {
    pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a>
    where 'a: 'b {
        Interface {
            manager: &mut self.manager
        }
    }
}

fn main() {

    let mut list = List {
        manager: Manager {
            text: "hello"
        }
    };

    list.get_interface().noop();

    println!("Interface should be dropped here and the borrow released");

    // 下面的调用可以通过,因为Interface的生命周期不需要跟list一样长
    use_list(&list);
}

fn use_list(list: &List) {
    println!("{}", list.manager.text);
}

To summarize: by changing the lifecycle annotation of get_interface, we've managed to shorten the lifecycle of the mutable reference borrowed by list.get_interface() so that it no longer lives until the end of the main function!
Notice that by making 2 code changes, or rather by changing the annotation of the function's lifecycle, we managed to shorten the lifecycle of the mutable reference borrowed by list.get_interface()!
This action illustrates the point that different lifecycle annotations give different lifecycles to references! That is, lifecycle annotations change the lifecycle of a reference!

Of course, there's really no need to look at such a complicated example, so I'll give a simple one below:

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = &5;       // -+-- 'b  |
        r = x;            //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

In the above code, we all know that the life cycle of x is shorter than that of r, so it causes the phrase println!(“r: {}”, r); to report an error.
Below, if I want you to fix this code, this is how you should go about it:

{
    let r;                // ---------+-- 'a
                          //          |
                          //          |
    let x = &5;           // -+-- 'b  |
    r = x;                //  |       |
                          // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

The above code simply removes {}, which expands the length of x's lifecycle, which can be described as changing x's lifecycle, not by way of annotation, but he illustrates the point that:
The lifecycle of a reference can be changed, it only requires the developer to change the appropriate code, or use change the lifecycle annotation of the reference to achieve, so, this is the key point of my discussion with you.
Note that I'm not looking at the problem from the point of view of running the program or compiling it, I'm looking at it from the point of view of the developer, and what I'm saying is that the developer can change the life cycle of a reference by changing the code, or by changing the life cycle annotation.
I mean: the developer can change the lifecycle of a reference by changing the code, or by changing the lifecycle annotations. Lifetime annotations can change how long any of the references live.

Lifetime annotations can change how lifetimes are inferred, yes.

You still haven't defined "life cycle". As mentioned above, the Book conflates Rust lifetimes ('_) with value liveness and lexical scopes. Given your example code block, perhaps you are to.

Anyway, the example compiles with the inner block. The &5 is const promoted. The lifetime ('_) in the type of x (and r) is not limited to the inner block (or outer block); it can be 'static (for both x and r) for example. Removing the inner block has no effect.[1]

Perhaps you meant this, where a borrowed value goes out of scope. In this case, removing the inner block still does not change any of the lifetimes ('_) in the analysis. Instead, a use of x -- namely, x going out of scope -- is moved from the end of the inner block to the end of the outer block. The use while borrowed is what causes the borrow check error. The compiler figured out x is borrowed at the end of the inner block because the inferred lifetime includes the println! that follows, due to the use of r.

Adding or removing blocks alone doesn't impose or remove lifetime ('_) constraints, but may add or remove uses (going out of scope, invoking destructors), which do interact with borrow checking.


  1. Here's a variation where annotations force the lifetime in the type of x to be longer than that of r. ↩︎

3 Likes

sorry,"life cycle"==“lifetimes”

That's what I'm focusing on, and I hope you can focus on this text as well, because it's a powerful refutation of the incorrect presentation of the official documentLifetime annotations don’t change how long any of the references live.

Having read and reread your opening post many times I'm still at a loss as to how there is ambiguity in the doc you have quoted.

"Lifetime annotations don’t change how long any of the references live."

I read that the same way I would read

"Writing a height in centi meters on the side of a truck does not actually change the height of the truck"

One thing that I always suspected might be confusing about discussing lifetimes with beginners is that data, as in variables, have a lifetime determined by how your program creates and disposes of them as it runs but lifetime annotations actually go on references to that data not the data itself.

(Lets ignore the case of a defence actually being a piece of data in it's won right).

Lifetime annotations do not actually specify any actual length of a life time of any data the refers to. Hence the next part of the quote in your OP

"Rather, they describe the relationships of the lifetimes of multiple references to each other..."

6 Likes