Lifetimes: Documentation

I have come from C++. I am used to its memory models. Now Rust. (loving it!)

I get what the borrow checker is doing until I do not. What has really got me worried is lifetimes. I recently wrote some code that was IMO syntactically and semantically correct. But for what seemed to me random reasons to do with lifetimes it did not compile. Then I made what still seem to me to be random changes (including edition = "2018" whatever that is!) and now it compiles.

I have read the book's chapters on lifetimes. I have asked many random questions here about life times. I have read a few blog posts.

I am confused. Deeply puzzled. I am inserting life times, almost at random. When the compiler accepts it, I stop. This is not sustainable!

So my question is to people who, like me, have come to Rust from C++ and have had to learn lifetimes and learn to love the borrow checker. What resources have you found useful?

I am wistful for The Design and Evolution of C++ and The C++ Programming Language by Bjarne Stroustrup . The illuminated so much for me back in the day....

Hmm, I went from C# full time to C++ for a few months, to Rust until now (about 3 months), and what I have found the most useful is just trying to avoid them. It's usually pretty easy to understand the basics of lifetimes, (scope 'a contains all variables with a &'a reference etc.), but sometimes, as with the previous thread, lifetimes come back to bite you in the butt. Just try to solve a problem without using lifetimes initially, and then, if unsolvable or too complicated or necessary to use Rc types, try to think of a way to solve it with lifetimes, which is usually the way to go.
For example, with your previous thread, I would have gone with either a restraint for 'a to be 'a: 'static, or have used a String. And for the add_child method, I would have gone with something like so:

fn add_child(&mut self, child:Node<'a>) -> Node

and force Node to implement clone. This allows for what I presumed you were going for and allows for this:

my_node.add_child(/**/)
       .add_child(/**/)
       .add_child(/**/)

etc.

The lifetime issue was fixed on the 2018 version because that updated how lifetimes are handled to be more lenient and allow more correct programs. This change is called non lexical lifetimes or nll for short.

What is foxed?

1 Like

If it compiles with edition = "2018", it's not you, it's Rust.

In previous version the borrow checker used simpler scope-based (lexical) rules, which were less flexible and rejected many borrows that were safe, but just didn't fit the simple rules.

4 Likes

Typo :sweat_smile:

Lifetimes are unavoidable if I wish to avoid cloning and copying data all over the place hen I do not need to.

Efficiency matters. Especially as Moore's Lore has finally run out of steam

1 Like

I imagine it is too soon to get the sorts of resources I used in 1997 for C++ (those were the days, men were boys and compilers sank ships...)

But I am hoping that some writer out there has a good book to sell me, with a well compiled index, that says everything once, clearly, in a sensible order.

Hope is free.... (I am not grumpy. Rust is new, and it is hard....)

I am a bit disturbed by the edition = "2018" that I had no idea such syntax even existed. How do I keep on top of that stuff?

To me it helped to realize a few things:

  1. Lifetimes don't do anything. They can't make a value live longer. They're just a fancy assert() that describes and checks what the code does anyway. They have zero influence on code being compiled.

  2. The compiler needs to trace the path from every borrow (&foo) all the way back to the owned memory that it came from (Vec, String, Box and other owned UpperCase Types) so that it can check it's used properly.

    For cases like fn foo(&self) -> &str it implies fn foo<'a>(&'a self) -> &'a str which means that the returned type came from something inside self. If there were more arguments, you may want to mark another input as related to the output. For structs with Foo<'a> that means the struct depends on something marked with that 'a. If you trace all of them hop by hop you'll be able to draw an imaginary line between every reference and memory it points to.

  3. The compiler is brutally pedantic about &mut. Use & and owned values wherever you can.

  4. Rust's references are not like C pointers, and using Rust's references in places where you'd use a pointer may not make any sense at all (e.g. Box<T>, not &T, is used to return newly-allocated values by reference).

3 Likes

I thought lifetimes attached references to each other. Maybe also attaches references to other objects. Semantically.

We could go around and around question and answer....

Better for me to read a book. Hence the question!

I think a useful exercise is to try to avoid complicated lifetime annotations at all. In most cases, if you don't do something especially complicated, you won't need more than one distinct lifetime parameter at any location. So just annotate everything with the same lifetime (like in struct declarations containing references) and allow the compiler to infer lifetimes (in function signatures).

If you do that and your code doesn't compile, it's possible that you're doing something non-trivial that requires a more complex lifetime annotation, but it's far more likely that you just try to do something that's plainly impossible (or difficult) to do in Rust - for example, if you try to return a reference that doesn't borrow from inputs:

// this won't compile:
fn prefix(string: String, index: usize) -> &str {
    &string[..index]
}

In this case, it will be more useful to think about how you can change the concrete types of your interface (e.g. make the function's argument accept a reference instead of an owned value), rather than trying to fix the issue by changing lifetime annotations (because your code won't work no matter how you annotate it).

// compiles:
fn prefix(string: &str, index: usize) -> &str {
    &string[..index]
}

Using this strategy for a while will help you understand the borrow checker better.

2 Likes

A document that may help in understanding ownership, borrowing, lifetimes and interior mutability is section 2  A Tour of Rust, in the paper RustBelt: Securing the Foundations of the Rust Programming Language, which was presented at RustBelt early in 2018.

Thanks. I will read that

Lifetimes are useful when you have a function that takes in multiple references, if you give them all the same lifetime (like &'a) then the lifetime associated with those references is the shortest scope of all the references.
If you give each input reference a different lifetime then instead of it being the shortest scope of all the references, each lifetime will refer to a different scope, or portion of the program.

Since your references have to be in scope to use them all the input references will share some portion of scope but the length of that scope can vary, that's why sometimes you may need different lifetimes assigned to references instead of the same lifetime assigned to all of the input references.

I mostly seem use lifetimes when returning a reference, to tell the borrow checker which scope the returned reference will take, because sometimes the shortest scope out of all the input references isn't long enough for my purposes.

Besides function signatures, you will also need lifetimes when using references in struct's (they always need explicit lifetimes), and in impl blocks (although less now when using the 2018 edition since it will implicitly add them for you). But remember lifetimes are thrown away after the borrow checker runs and verifies that the program can run without any errors with references, they don't affect the code, only if the program is correct or not.