DEV Community

Cover image for Lifetimes in Rust Explained with Examples
Augustine Madu
Augustine Madu

Posted on • Originally published at cudi.dev

Lifetimes in Rust Explained with Examples

In this article, you are going to learn about lifetimes in rust, together with its purpose using examples.

For start, here we have a variable s, created at the outer scope, and at the inner scope, we have a variable t which is an integer 5, we then make s a reference to t.

fn main() {
  let s;

  {
    let t = 5;

    s = &t;
  }
}
Enter fullscreen mode Exit fullscreen mode

This compiles at the time, but the problem with this is that when t goes out of scope, all its references will become invalid, now s becomes invalid.

If we try to print the value of s, then we get an error.

fn main() {
  let s;

  {
    let t = 5;

    s = &t;
  }

  println!("{s}");
}
Enter fullscreen mode Exit fullscreen mode
error[E0597]: `t` does not live long enough
  --> lifetimes/src/main.rs:11:9
   |
9  |     let t = 5;
   |         - binding `t` declared here
10 |
11 |     s = &t;
   |         ^^ borrowed value does not live long enough
12 |   }
   |   - `t` dropped here while still borrowed
13 |
14 |   println!("{s}");
   |             --- borrow later used here

Enter fullscreen mode Exit fullscreen mode

Rust figures this out using lifetimes. Each variable has a lifetime associated with the scope in which it was created. Here, s has a lifetime which we will call a, and t has a lifetime which we will call b.

fn main() {
  // LIFETIMES

  //---------------------- 'a
  let s;               //|
                       //|
  {                    //|
  //---------------'b    |
    let t = 5;  //|      |
                //|      |
    s = &t      //|      |
  //---------------'b    |
  }                    //|
  //---------------------|'a
}
Enter fullscreen mode Exit fullscreen mode

When we assign s a reference to t, rust compares the lifetime of s and t and sees that the lifetime of s is longer than that of t, now when t goes out of scope, s becomes invalid.

Most times, we do not need to specify the lifetime of a reference. But to use a reference in a struct and sometimes in a function, you need to specify the lifetimes of the reference.

Let's look at the functions below to understand how to specify lifetimes in a function.

If you prefer a video version of this article. You can check out my YouTube video on it.

Example 1

For the first example, we try to return a reference for a variable created inside this function. But when the function scope ends the variable x is dropped and the reference becomes invalid, so this function returns an invalid reference therefore it wouldn’t compile.

fn example_1() -> &i32 {
  let x = 5;

  &x
}
Enter fullscreen mode Exit fullscreen mode
  --> lifetimes/src/main.rs:13:19
   |
13 | fn example_1() -> &i32 {
   |                   ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from 
   help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
   |
13 | fn example_1() -> &'static i32 {
   |                    +++++++
help: instead, you are more likely to want to return an owned value
   |
13 - fn example_1() -> &i32 {
13 + fn example_1() -> i32 {
   |
Enter fullscreen mode Exit fullscreen mode

If a function returns a reference that is not static, then it should come from one of its parameters.

The purpose of lifetimes in functions is to inform the compiler how the lifetime of its output values relates to the lifetime of its input parameters. We shall see this in the coming examples.

Example 2

For the second example, we return x from the function’s parameter which is a reference of an integer. It compiles successfully as the compiler assigns the lifetime of the return value of this function to that of x from its parameter.

fn example_2(x: &i32) -> &i32 {
  x
}
Enter fullscreen mode Exit fullscreen mode

But when we have two reference parameters and an output which is a reference, the compiler has no way of knowing how the lifetime of the output reference relates to that of the input.

Example 3

For the third example, here we have two reference parameters, now the compiler can not figure out which lifetime to assign the return value of the function, so we manually add a lifetime to the function.

fn example_3(x: &i32, y: &i32) -> &i32 {
  x
}
Enter fullscreen mode Exit fullscreen mode
error[E0106]: missing lifetime specifier
  --> lifetimes/src/main.rs:23:35
   |
23 | fn example_3(x: &i32, y: &i32) -> &i32 {
   |                 ----     ----     ^ expected named li
fetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
   |
23 | fn example_3<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
   |             ++++     ++          ++          ++

Enter fullscreen mode Exit fullscreen mode

We label the lifetime of a reference by adding an apostrophe after the ampersand followed by the letter we choose to label it with, in small cases.

&'a x
&'b mut x
Enter fullscreen mode Exit fullscreen mode

To add a lifetime in a function, we specify the label of the lifetimes used in the function in an angle bracket, then give each reference parameter and return values of their lifetimes after their ampersand.

fn example_3<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  x
}
Enter fullscreen mode Exit fullscreen mode

Now this tells the compiler that the return value lives as long as both parameters live. If one of the two is dropped, the output of the function is dropped too.

In some situations, the lifetime of the return value only relates to some and not all the lifetime of the input parameters.

Example 4

For the fourth example, we return x from the parameter, and we want to specify that the lifetime of the return value only relates to that of the first parameter. So we add two lifetimes a and b for the function. We label the lifetime of x with a, and that of y with b. Since the lifetime of the return value relates to that of x, we label its lifetime with a as well.

fn example_4<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
  x
}
Enter fullscreen mode Exit fullscreen mode

If we try to return y, we get an error, since the lifetime of y and the return value are different.

fn example_4<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
  y
}
Enter fullscreen mode Exit fullscreen mode
error: lifetime may not live long enough
  --> lifetimes/src/main.rs:28:3
   |
27 | fn example_4<'a, 'b>(x: &'a i32, y: &'b i32) -> &'...
   |              --  -- lifetime `'b` defined here
   |              |
   |              lifetime `'a` defined here
28 |   y
   |   ^ 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`
Enter fullscreen mode Exit fullscreen mode

To see this further, in the main function, we have a variable x, and z at the outer scope and y at the inner scope.

At the inner scope, we assign z to be the return value of the function, outside the inner scope, we print the value of z and this compile successfully, since the lifetime of z is tied to that of x at the first parameter.

fn main() {
  let x = 2;

  let z;

  {
    let y = 3;

    z = example_4(&x, &y);
  }

  println!("{z}");
}
Enter fullscreen mode Exit fullscreen mode

If y is at the first parameter and x is at the second, the lifetime of y will then be tied to the return value of the function, and when we try to use z outside the scope of y we get an error.

Example 5

For the fifth example, here, we have one reference parameter and the return value is a tuple containing two integer references. We do not need to explicitly specify the lifetimes as the compiler assumes that any reference in the output comes from this one parameter, so this compiles successfully without any errors.

fn example_5(x: &Vec<i32>) -> (&i32, &i32) {
  (&x[0], &x[1])
}
Enter fullscreen mode Exit fullscreen mode

Example 6

For the sixth example, instead of just one reference parameter, we have two reference parameters. The compiler has no idea how the reference in the output relates to that of the input. The two references in the output can come from the first, the second, or both.

fn example_6(x: &Vec<i32>, y: &Vec<i32>) -> (&i32, &i32) {
  (&x[0], &y[0])
}
Enter fullscreen mode Exit fullscreen mode

In our implementation, the first element of the tuple comes from the first parameter, while the second element of the tuple comes from the second parameter. To specify this in lifetimes, since we are dealing with two different lifetimes, we add two lifetimes labels to the function, and the lifetime we give to the first parameter, we give to the first element of the tuple, and we do that for the second parameter and second tuple.

fn example_6<'a, 'b>(x: &'a Vec<i32>, y: &'b Vec<i32>) -> (&'a i32, &'b i32) {
  (&x[0], &y[0])
}
Enter fullscreen mode Exit fullscreen mode

Static Lifetimes

The next lifetime to talk about is the static lifetime. This is a lifetime of a variable that can live for the entire duration of the program. All string and byte literals have a static lifetime since they are stored in the program's binary which is always available.

Unlike our first example, this program compiles since the return value of this function can live as long as the program, we just have to specify that the lifetime of the return value is static using the static lifetime.

fn example_static() -> &'static str {
  let x = "hello";

  x
}
Enter fullscreen mode Exit fullscreen mode

Specifying Lifetimes in a Struct

To use a reference in a struct that isn’t a static lifetime, we have to specify the lifetimes used in the struct in an angle bracket as well.

// we specify the lifetimes being used in an angle bracket
struct Person<'a> {
  name: &'a str,
}
Enter fullscreen mode Exit fullscreen mode
// we don't add static lifetime in angle bracket
struct Car {
  color: &'static str,
}
Enter fullscreen mode Exit fullscreen mode

When creating methods for this struct, we add after the ‘impl’ keyword as well.

impl<'a> Person<'a> {
  fn new() -> Self {
    todo!()
  }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for reading.

An article from my website https://cudi.dev/articles/lifetimes_in_rust_explained_with_examples

Top comments (0)