When I name a lifetime, what's its scope?

I understand the basics of lifetimes, apart from the question in the title! But in my Rust learning project have found it best to try to avoid introducing them. Whenever I do so by following the compiler suggestion to introduce a lifetime specifier, I find this spreads like a virus from one struct to the next and I don't yet understand whether it's worth the complex interdependencies that this seems to create.

So I generally prefer to back out and find ways to code that avoid specifying lifetimes. But maybe I just lack some overall understanding, and need to read up on how to use them?

Is there a blog that discusses the pros and cons of introducing lifetimes and gives guidance on when to use/not use them?

Thanks, oh and the question in the title: what's the scope of a lifetime identifier?

Note that lifetimes are an integral part of all Rust code, although most of them are never explicitly named. A lifetime identifier acts very much like a generic type parameter, and in particular its scoping rules are the same.

Instead of identifying a type, however, it’s effectively naming the stack frame that owns some referenced data. They “infect” other types in the same way type parameters do: everything that wants to use a generic type needs to either be generic itself or provide concrete values for the generic parameters. In the case of lifetimes, however, that concrete value can only come from the body of a function that owns the data being referenced.

Types that take lifetime parameters tend to be short-lived views of other data. For long-term storage, you’re usually better off with some combination of clone() and Rc/Arc.

3 Likes

this spreads like a virus from one struct to the next

That's because you're trying to store things by reference using & temporary borrows, rather than Box (or other types like Arc, Vec, String) which still provide indirection, but own their data.

The problem is you try to use references for not-copying, but in Rust reference means not-owning. There are many other not-copying types without the highly problematic non-owning aspect.

Everything that isn't owned by a struct, is temporary, and has to be permanently and very strictly chained to the variable it came from. Rust will force you to add a trail of lifetime annotations as breadcrumbs from the variable that has been borrowed, through all the structs and functions to every single use of values borrowed from that variable. It infects the whole codebase, because you're trying to hang your codebase on some poor variable somewhere.

Don't use temporary references in structs. Use owning types. They don't have lifetimes, and don't cause the spread of annotations that paralyze codebases. Owning types can be used and passed everywhere without any annotations.

9 Likes

A lifetime is used to mark things that reference values stored and owned outside the struct. This makes the struct temporary, and bound to this other things scope, as the reference must stay valid.

Generally the scope of a generic lifetime on a struct is a region that contains the entire region in which the struct can exist. The scope of a generic lifetime defined on a function is a region that contains the entire body of the function. Here "contains" means "equal to or larger".

1 Like

I think the important thing to realize here is that the actual life times of variables is determined by the structure of your program, where/when those variables are created and where when they are destroyed.

A typical local variable in a function, say some struct, only lives as long as that function takes to run. The variable comes into life when you declare it and dies when the function returns.

You could pass references down though calls to a thousand functions to a call depth of a hundred, say, and all those called functions are referencing something with a lifetime of the original top level function. When everything returns back up the call stack and out the top level function the variable is gone.

So life time is infectious that way. Because of how you wrote your code.

With that in mind we see that adding life time, tick mark, annotations to a variable does not "specify" or change it's life time at all. They are required sometimes to help the compiler when it cannot figure things out, when life times are ambiguous.

Given the above we see that the dependencies are not created by the lifetime tick-marks, they are inherent in your code structure.

Luckily in all my application code I don't think I have even one life time tick-mark. When data needs to hang around longer than the scope of it's creation it can be wrapped in some smart pointer, an Rc or Arc, say. Or one can use .clone() to replicated it around the place, provided that is not too expensive an operation.

If you can stand watching a live coding video for an hour Jon Gjengset has an excellent exposition of life times here: " Crust of Rust: Lifetime Annotations": https://www.youtube.com/watch?v=rAl-9HwD858&t=4663s. He has a great style.

2 Likes

Thank you everyone, several useful nuggets there and pointers both on how to think about this and where to look for more guidance. Excellent!

I need to dedicate some more time to understanding when and how to use explicit lifetimes.

I am getting the impression that most code will be cleaner without them!

This is an interesting area for me. I had understood that I could clone some things so each struct has its own copy in some use cases, but need to learn more about the impact of this in each type, and of using smart pointers. I've tried to be sparing with use of clone() because I believe it can store up problems for later which might be hard to address without significant refactoring.

1 Like

I have written a rather substantial simulator for a distributed system and have not had any problems with using clone(), not even performance problems. The only thing that bothers me is the visual clutter, which is even worse with the alternative of reference counted types.

1 Like

I have come to some conclusions after many decades of programming:

  1. Is that commonly known idea about "premature optimization". Don't waste time fiddling with code in the hope of optimizing it's performance. If it turns out there is a performance problem, then look at at it.

  2. Less talked about is what I might call "premature generalization". You are solving a problem here and now. Why are you stressing your mind as to how your code could be extended this way and that for some unknown future requirements? If you do that you are likely making the "here and now" solution far more complex than it need be in the name of a future that will likely never come.

In short, just do it. If on the off chance that performance or feature requirements change do it over again. That is a different project.

An amazing feature of Rust though is that one can refactor old code, this way and that, and it will not let you make a mess.

2 Likes