Skip to the content.

Lifetimes

keywords : safety, borrow checker, dangling references

The concept of lifetimes is central to Rust’s ownership and borrowing system, which ensures memory safety without the need for a garbage collector.

Every reference in Rust has a lifetime, which is the scope for which that reference is valid. The Rust compiler’s borrow checker will compare scopes to determine whether all borrows are valid.

Wherever borrowing happens, the lifetime should be considered.

Most of the time, lifetimes are implicit and inferred. But we must annotate lifetimes when the lifetimes of references could be related in a few different ways.

The Main Aim of lifetimes is to prevent dangling references, which cause a program to reference data other than the data it’s intended to reference.

For furture reading:
Validating References with Lifetimes

Key Concepts of Lifetimes

Example: Scopes (i.e., the lifetimes)

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

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

Example: Lifetime Errors - Dangling References

Rust’s compiler enforces lifetime rules to prevent errors such as dangling references. (It is the main aim of lifetime). If a reference outlives the data it points to, Rust will produce a compile-time error, avoiding issues that could lead to undefined behavior.

fn dangling_reference() -> &String {
    let s = String::from("hello");
    &s  // Error: `s` does not live long enough
}

Example: How borrow checker works with lifetime

Consider a function that returns a reference to the larger of two integer slices:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The following examples show distinct scenario. One will pass the check and the other will not pass it. It shows that lifetimes help to prevent dangling references. With lifetimes, the borrow checker ensures data outlives its references.

// Example: pass the valid check
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

// Example: fail
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str()); // <-- voilate lifetime contract
    } // <--- if it does not fail, here will introduce dangling references
    println!("The longest string is {}", result);
}
// result:
$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

Example: Lifetime Elision

The compiler uses three rules to figure out the lifetimes of the references when there aren’t explicit annotations.

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

The lifetime elision rules applies:

fn first_word<'a>(s: &'a str) -> &'a str {

Now all the references in this function signature have lifetimes, and the compiler can continue its analysis without needing the programmer to annotate the lifetimes in this function signature.

Example with Partially Annotated Lifetimes

The way in which you need to specify lifetime parameters depends on what your function is doing.

case 1

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

We wouldn’t need to specify a lifetime on the y parameter because the lifetime of y has no relationship with the lifetime of x or the return value.

case 2

pub fn search<'a>(query: &'a str, contents: &str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// result:
error[E0621]: explicit lifetime required in the type of `contents`
  --> src\lib.rs:41:5
   |
32 | pub fn search<'a>(query: &'a str, contents: &str) -> Vec<&'a str> {
   |                                             ---- help: add explicit lifetime `'a` to the type of `contents`: `&'a str`
...
41 |     results
   |     ^^^^^^^ lifetime `'a` required

The returned reference has to do with the ‘contents’ parameter. The user shall specify lifetime to tell their relationship. And then borrow checker will compare scopes to determine whether all borrows are valid. Otherwise the compiler will automatically give it a distinct lifetime that does not tell that relationship. Just like the following:

pub fn search<'a, 'b>(query: &'a str, contents: &'b str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// result:
error: lifetime may not live long enough
  --> src\lib.rs:41:5
   |
32 | pub fn search<'a, 'b>(query: &'a str, contents: &'b str) -> Vec<&'a str> {
   |               --  -- lifetime `'b` defined here
   |               |
   |               lifetime `'a` defined here
...
41 |     results
   |     ^^^^^^^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
   |
   = help: consider adding the following bound: `'b: 'a`

As we can see, the lifetime of contents parameter has no relationship with the lifetime of returned value. It is not the case.

Our solution is to specify lifetimes to make clear the relationship.

// solution 1
pub fn search<'a, 'b>(query: &'a str, contents: &'b str) -> Vec<&'b str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// solution 2
pub fn search<'b>(query: &str, contents: &'b str) -> Vec<&'b str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

Summary

Lifetimes are a way of telling the Rust compiler how long references should be valid.