Rust lifetimes usage

Based on collective experience of folks in this list - how often do you find yourself using lifetime annotation explicitly? I am involved in this project pretty sizable and i don't ever find the need to use them nor does anyone else. I mean does that imply a lack of depth in the usage. Just curious.

Thanks in advance

Explicit lifetime annotations on functions are rarely needed, because the elision rules cover like 99% of the most sensible patterns.

Explicit lifetime annotations on user-defined types are needed when the purpose of your UDT is to hold short-lived references to another value, e.g. some sort of a RAII guard such as a transaction or lock. Lifetime parameters on traits may be needed to allow the trait to be implemented on borrowed types (at all, or efficiently, without cloning).

I think, as a consequence, the amount of lifetime annotations you will need to write depends on the project. In general, beginners tend to over-use lifetimes and fewer annotations are better than more.

However, if you literally never wrote a single lifetime in your say 10k LOC project, you might be cloning things a bit too much, or may not be abstracting away the important concepts using UDTs and traits. Or maybe you just got really lucky and you truly never needed them (eg. you are performing a lot of numerical computation but there is not much domain modelling to be done).

What kind of project are you working on?

3 Likes

Relatively rarely.

I've just checked one project. It has 35000 lines of code, 2700 functions/structs/enums, and <'[a-z] appears on 260 lines, in 53 out of 141 files.

2 Likes

It looks like I nailed the 99% figure. :sweat_smile:

1 Like

So I had to check my current little project, it might make it to 6K lines when done. There is not even one lifetime tick mark in there. There is a dozen `clone´ in there but they are all cloning mpsc tx ends, which is what one is supposed to do.

Recently I was watching a presentation by Alice on "actors" in Rust and ownership. There it was shown that use of lifetimes was avoided by structuring the actors in a certain way.

So far my feeling is that if I find myself needing to use lifetime tick marks it's a warning that I'm going down the wrong path in my code structure.

1 Like

Again, YMMV. Data structures ought to be used primarily by-value. Similarly, numerics-heavy code where all you do is iterate over &[f64] and return a single f64 isn't going to need lifetimes. Or if you are writing a highly abstract, mathematical crate such as typenum, where all you have are unit placeholder types, and you don't care about any lifetime because you don't care about values, only types. Or if you are making an HTTP API and returning responses, you better return them by-value.

However, one of my main projects nowadays is a database abstraction layer, which does need a lot of lifetimes in order to avoid cloning big blobs and strings just to pass them to a database (which is going to clone and/or serialize them again, anyway), and to implement transaction guards. It all depends on what you are doing.

1 Like

I use lots of lifetimes, but I create a lot of systems, which tends to involve more thinking about how lifetimes will flow through it, and also I like challenging myself to creating the systems in ways that avoid heap allocations except in start up and other rare events, which of course pushes me to use lifetime stuff more heavily. But I find that when actually creating code that's close to "the top" of the layers of abstraction, that's more of an end user application of systems, it rarely involves explicitly writing out the lifetimes.

Lifetimes are an "if you have to" feature, so if you don't need them then great!

Often writing code without them works out better. For example, the obvious min function would be something like this:

fn min<'a, T: Ord>(a: &'a T, b: &'a T) -> &'a T { ... }

where lifetime elision intentionally doesn't apply because the function really does want both parameters to have the same lifetime.

But that's not what the best min function looks like. Instead, it's better to do

fn min<T: Ord>(a: T, b: T) -> T { ... }

without needing to mention lifetimes at all, because a reference with a normal lifetime is a perfectly normal type that can be passed to that anyway.

(And as an extra bonus, this second implementation is actually correct for DSTs too -- the first one should have been T: ?Sized + Ord, which is something you don't need to worry about if the generic is the full type rather than the thing under the reference.)

That does need Ord to be implemented for references, but for extra fun that implementation didn't need to 'a anything either: https://doc.rust-lang.org/1.74.0/src/core/cmp.rs.html#1606-1615

    impl<A: ?Sized> Ord for &A
    where
        A: Ord,
    {
        #[inline]
        fn cmp(&self, other: &Self) -> Ordering {
            Ord::cmp(*self, *other)
        }
    }

So while lifetimes are sometimes really important -- don't avoid them when they're useful, especially if avoiding them means excessive copying or refcounting -- trying to not write them out yourself can be a nice tool to find a cleaner way of doing something.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.