This article was originally published by LogRocket on their blog under
the title /Understanding lifetimes in Rust/. You can find their
version here.

You sit down for another crack at this Rust-thing. Last time went pretty smoothly except for some minor hiccups with the borrow checker. But you got through it and gained a slightly better understanding of how it works in the process. Maybe it's all worth it in the end?

Today, though, you've got some grand plans and you're not going to let the borrow checker stop you! You can practically feel the energy coursing through your veins as you imprint your thoughts on the keyboard and translate them into pure Rust. This must be that sweet feeling you've heard so much about. You save your project, start the compilation process, and ...

error[E0597]: `x` does not live long enough

You sigh. Not this again.

You take a deep breath, lower your shoulders, and read the error message one more time. 'Does not live long enough'? What does that even mean?

Introducing lifetimes

You've run into another one of Rust's peculiarities: lifetimes.

Lifetimes are how the Rust compiler keeps track of how long references are valid for. If you remember the article on understanding the borrow checker, you'll know that checking references is one of the borrow checker's main responsibilities. Lifetimes help the borrow checker ensure that you never have invalid references.

Lifetime annotations, then, let you tell the borrow checker how long references are valid for. In many cases, the borrow checker can infer the correct lifetimes and take care of everything on its own. But often it needs your help to figure it out.

In this post, we'll go over the basics of lifetimes and annotations and how to work with them. We'll also look at some common scenarios you might run into and how you can solve them with lifetimes. I'm expecting a basic grasp of Rust and some if its concepts (such as the borrow checker), but nothing particularly deep.

Lifetime annotations

Before we go any further, just a short note on the notation of lifetimes, as it's a bit different from what you get in a lot of other languages.

Lifetimes are annotated by a leading apostrophe followed by a variable name. When talking about generic lifetimes, we often use single, lowercase letters, starting from ~'a~, ~'b~, etc. However, there is nothing stopping you from using longer, more explanatory names if that suits you better.

Why we need lifetimes

If they're such a weird feature, then why do we need lifetimes? The answer, my friend, lies in Rust's ownership model. The borrow checker takes care of allocating and freeing memory and also of making sure that no references point to memory that has been freed. Like borrows, lifetimes are checked at compile-time, which means that your program can't compile if the borrow checker deems the references invalid.

In particular, lifetimes are important to keep in mind when returning references from functions and when creating structs with references. These are both common situations, and it's easy to get lost if you don't understand what's going on.

The explanatory example

Ultimately, lifetimes are a matter of scope. Values get dropped when they go out of scope and any references to them after they have been dropped are invalid.

The simplest way to demonstrate lifetimes is something like the following example, shamelessly stolen/adapted from the Book's chapter on lifetimes.

// this code sample does *not* compile
{
    let x;
    {                           // create new scope
        let y = 42;
        x = &y;
    }                           // y is dropped

    println!("The value of 'x' is {}.", x);
}

This little piece of code has two distinct scopes. When the inner scope closes, y is dropped. At that point, even if x is still available in the outer scope, the reference is invalid because the value it pointed to is dropped: The value that x points to 'does not live long enough'.

In lifetime jargon, we can say that the outer scope has the lifetime ~'outer~ and the inner scope the lifetime ~'inner~. ~'outer~ clearly outlives ~'inner~ in this case. When ~'inner~ ends, all values with that lifetime are invalidated.

Lifetime elision

When writing functions that accept references as arguments, the compiler can infer the correct lifetimes in many cases, saving us the trouble of writing them out by hand. When lifetime annotations are implicit, we call this lifetime elision.

The compiler uses three rules to figure out whether lifetime annotations can be elided or not. The section on lifetime elision talks about these rules in detail, but the short form is that you can elide lifetime annotations in functions if one of the following is true:

  • The function doesn't return a reference.
  • There is exactly one reference input parameter.
  • The function is a method, taking &self or &mut self as the

first parameter.

Examples and common problems

Lifetimes are a tricky thing to wrap your head around, and it's unlikely that a wall of text will make you really understand how they work. The best way to get a proper understanding is of course to play around with them yourself and solve problems, but a couple of examples can go a long way.

Returning references from functions

You can't return a reference from a function without also passing in a reference. If you try, you'll find that the reference is invalid as soon as the function returns and your program won't compile[1].

If your function takes exactly one reference parameter, then you'll be fine without annotations. All output references will be given the same lifetime as the input parameter. As such, this simple function will compile just fine, even if there are no explicit lifetime annotations:

fn f(s: &str) -> &str {
    s
}

However, if we add another input string parameter (even if we don't use it), we'll suddenly not be able to compile this:

// this code sample does *not* compile
fn f(s: &str, t: &str) -> &str {
    if s.len() > 5 { s } else { t }
}

This is because of how the automatic lifetime annotation works. When a function accepts multiple references, they're each given their own lifetime. We know that the returned reference must be one of the references we received as an input argument, but we don't know which one. What goes in place of the ~'???~ below?

// this code sample does *not* compile
fn f<'a, 'b>(s: &'a str, t: &'b str) -> &'??? str {
    if s.len() > 5 { s } else { t }
}

Imagine that we want to use the returned value outside of this function. What lifetime would we assign to it? The only thing we can guarantee is that the reference we return is valid for at least as long as the shortest-lived reference we pass into the function. That tells the compiler that these two references are definitely valid for the shorter lifetime. We cannot give any guarantees outside of that.

The way we achieve this, is by giving both input parameters the same lifetime annotation. It's how we tell the compiler that 'as long as both of these input parameters are valid, so is the returned value'.

fn f<'a>(s: &'a str, t: &'a str) -> &'a str {
    if s.len() > 5 { s } else { t }
}

If you're returning a reference from a function that takes multiple input lifetime parameters, but you know exactly which one it is you're returning, you can annotate that specific lifetime. That way, the relationship between the lifetimes doesn't matter anymore.

fn f<'a, 'b>(s: &'a str, _t: &'b str) -> &'a str {
    s
}

Structs with references

References in structs can be a real hassle. You're often better off avoiding them and using owned values instead. That way, you don't need to worry about references being invalidated and lifetimes not lasting long enough. In my experience, it's usually also what you want.

However, there are certain cases where structs with references are exactly what you want. In particular: if you want to create a 'view' into something else. Using structs with references is a great way of organizing some data into a package that's easier to handle, without moving or copying data. This means that the original data source can still be referenced elsewhere and that we save on the extra work it would be to clone the data.

Here's an example. Imagine that we want to find the first and the last sentence of a paragraph and keep them in a struct S. Because we don't want to copy the data, we need to use references and give them lifetime annotations.

struct S<'a> {
    first: &'a str,
    last: &'a str,
}

We could use a function like this to populate the struct. For simplicity's sake, we'll assume that a full stop is the only sentence-ending punctuation mark in use. If the paragraph is empty, we'll return None, and if there is only a single sentence, we'll use that as both the first and the last sentence:

fn try_create(paragraph: &str) -> Option<S> {
    let mut sentences = paragraph.split('.').filter(|s| !s.is_empty());
    match (sentences.next(), sentences.next_back()) {
        (Some(first), Some(last)) => Some(S { first, last }),
        (Some(first), None) => Some(S { first, last: first }),
        _ => None,
    }
}

Notice how we don't need to annotate lifetimes in the function signature because the compiler can figure it out for us. In a case like this, there is really only one choice: the lifetime of the input string. Pretty neat, huh?

Summary and further reading

This has been a cursory glance at lifetimes and lifetime annotations. We have glossed over a lot of the finer and more intricate details of how lifetimes work, but we've covered enough ground that you should be able to reason about them when you run into an issue.

Because lifetimes are such an important part of Rust, I encourage you to have a look at the Validating References with Lifetimes chapter of The Book if you want a more comprehensive introduction.

Furthermore, if you feel like you've got a decent grasp on lifetimes but want to dive a bit deeper, check out Jon Gjengset's excellent video Crust of Rust: Lifetime Annotations, where he explores a case that needs multiple explicit lifetime annotations. He also gives a great introduction to lifetime annotations in general, so it's well worth a watch just for that.

Footnotes

[1]

Well, you can use the ~'static~ lifetime, but that's probably not what you want. It's also outside the scope of this article, so let's forget about that for now.

[1]

Well, you can use the ~'static~ lifetime, but that's probably not what you want. It's also outside the scope of this article, so let's forget about that for now.



Thomas Heartman is a developer, writer, speaker, and one of those odd people who enjoy lifting heavy things and putting them back down again. Preferably with others. Doing his best to gain and share as much knowledge as possible.