Lifetimes

Lifetime Basics

Rust by example - Lifetimes

In Rust, references allow you to access a value without taking ownership of it ("borrow" it). There are immutable references (of type &T), which allow read-only, shared access to a value; and mutable references (of type &mut T), which allow modification but enforce exclusive access.

References are similar to pointers but come with strict safety guarantees: they are aligned, not null, and pointing to memory containing a valid value of T.

To guarantee the latter, the Rust compiler attaches a lifetime to each reference. A lifetime represents the scope (a section of code) for which the value (and therefore the borrow) is valid. Lifetimes therefore prevent dangling references, which occur when a reference points to memory that has been deallocated or otherwise invalidated.

The Rust compiler infers most lifetimes (see below), therefore we will most often write references with an implicit lifetime:

  • &i32: a shared reference.
  • &mut i32: a mutable reference with an implicit lifetime.

When the compiler can't figure out the relationships between the lifetimes of different references (especially in function signatures or struct definitions), you need to provide explicit lifetime annotations:

  • Lifetime names are always prefixed with an apostrophe (e.g., 'a, 'b, 'static). By convention, short, lowercase names are usually used. 'static is a special lifetime that means the reference is valid for the entire duration of the program.
  • When added to references, the lifetime annotation is inserted after & and before the mut keywords or the type:
    • &'a i32: a shared reference with an explicit lifetime 'a.
    • &'a mut i32: a mutable reference with an explicit lifetime 'a.

Note the following:

  • Lifetimes are annotations, not types: Lifetimes don't change the underlying type of a variable. A &i32 is a reference to an i32, regardless of its lifetime annotation. The lifetime annotation just provides extra information to the compiler about how long that reference is valid.
  • A lifetime is said to "outlive" another one if its representative scope is as long or longer than the other. References with longer lifetimes can be freely coerced into references with shorter ones. As such, you should read a lifetime 'a as "lives at least as long as 'a".
  • Lifetimes are compile-time only: they are erased during compilation and have no runtime overhead.

Lifetime Elision Rules

As discussed above, Rust's compiler infers most lifetimes. More specifically, it has "lifetime elision rules" that allow you to omit them in common, unambiguous cases. This reduces boilerplate.

  • Each input lifetime parameter gets its own lifetime parameter. fn foo(x: &str) is automatically treated as fn foo<'a>(x: &'a str).
  • If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters. fn foo(x: &str) -> &str is treated as fn foo<'a>(x: &'a str) -> &'a str.
  • If there are multiple input lifetime parameters but one of them is &self or &mut self, the lifetime of self is assigned to all output lifetime parameters.

These rules cover many common scenarios, but you will need lifetimes in more complex cases:

  • Functions that take multiple references and return references. When a function takes multiple references as input and returns a reference that could be based on any of those input references, the compiler needs to know the relationship.
  • Structs that hold references. If a struct needs to hold references, it must be annotated with lifetime parameters to ensure that the references it holds don't outlive the data they point to.
  • Traits that involve references. When defining traits that include methods returning references, you need to specify lifetimes to ensure the returned references are valid for the expected duration.

Use 'static as the Lifetime of the Running Program

'static indicates that the data pointed to by the reference lives for the lifetime of the running program. It can still be coerced to a shorter lifetime. String literals are a common example of a string slice with 'static lifetime.

fn my_string() -> &'static str {
    // This string literal is stored directly in the binary,
    // and therefore has a `'static` lifetime.
    let s: &'static str = "I have a static lifetime.";
    s
}

fn main() {
    println!("{}", my_string());
}

Use Lifetime Parameters

Lifetime parameters can be added to function or method signatures, struct definitions, enumerations, unions, impl blocks, type aliases, traits, in the same way that a generic type parameter or constant can be added. They are used to specify the relationships between the lifetimes of different references in the function, type, or item.

Lifetime parameters are listed in angle brackets (e.g. <'a>), usually immediately after the name of the item. Lifetime parameters are a form of generics. Lifetime parameters, type parameters, and const generics can be intermixed within <...>. Lifetime parameters should appear before any generic type and const parameters (e.g. <'a, T, const N: usize>):

#![allow(dead_code)]
//! Example of lifetime parameters in various items.

// Function with a lifetime parameter `'a`.
// The parameter is used by the function parameter declaration.
// In this case, `s1`, `s2`, and the return value all share the same lifetime.
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

// Type alias with a lifetime parameter:
type StrRef<'a> = &'a str;

// Struct with lifetime and type parameters:
struct RefHolder<'a, T> {
    reference: &'a T,
}

// Implementation for the struct, with lifetime and type parameters.
// Note how the lifetime parameter appears directly after `impl`.
impl<'a, T> RefHolder<'a, T> {
    fn new(reference: &'a T) -> Self {
        Self { reference }
    }
}

// Enumeration with lifetime parameter:
enum Either<'a, T, U> {
    Left(&'a T),
    Right(&'a U),
}

// Union with lifetime parameter (unsafe, rarer):
union MyUnion<'a> {
    int_ref: &'a i32,
    float_ref: &'a f32,
}

// Trait with a lifetime parameter:
trait Describable<'a> {
    fn describe(&self) -> &'a str;
}

// Implementation of a generic trait, with a lifetime parameter:
impl<'a> Describable<'a> for String {
    fn describe(&self) -> &'a str {
        "This is a string."
    }
}

use std::fmt::Display;

// Implementation of a trait for a generic type.
// Note the trait bound.
impl<'a, T: Display> Display for RefHolder<'a, T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RefHolder: {}", self.reference)
    }
}

// Implementation of a generic trait for a generic type:
impl<'a, T> Describable<'a> for RefHolder<'a, T> {
    fn describe(&self) -> &'a str {
        "This is a reference holder."
    }
}

fn main() {
    let s1 = "Hello";
    let s2 = "World!";
    println!("Longest: {}", longest(s1, s2));

    let num = 42;
    let holder = RefHolder { reference: &num };
    println!("{}", holder.describe());
}

Use Lifetime Parameters in Functions

In the following example, we define a function with a lifetime parameter, which is then used to specify the relationships between the lifetimes of different references. In this case, the generic lifetime parameter will get the concrete lifetime that is equal to the smaller of the lifetimes of the function arguments x and y:

/// This function has a lifetime parameter `'a`.
/// The lifetime parameter can be used by references within the function
/// signature, return type, or body.
///
/// In this example, the function takes two string slices, `x` and `y`, both
/// with the same lifetime `'a`. It returns a string slice that is the longer of
/// the two, also with the lifetime `'a`.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    // We define two variables, one with a scope / lifetime
    // shorter than the other.
    let x = String::from("looooooong");
    let l;
    {
        let y = String::from("short");
        l = longest(&x, &y);
        println!("Longest: {l}");
    } // y is dropped here.
    // println!("Longest: {l}"); // ERROR: borrowed value does not live long
    // enough.
    println!("x: {x}");
    // x is valid until this line.
}

Use Lifetimes and Lifetime Parameters in Struct Definitions and Implementations

The following example shows a struct with a lifetime parameter and multiple implementations, two for any lifetimes, one for a specific lifetime. This is a form of conditional implementation.

/// A struct that holds a reference to a string slice.
struct Excerpt<'a> {
    part: &'a str,
}

/// An implementation only defined for the `'static` lifetime.
impl Excerpt<'static> {
    fn only_if_static(&self) {}
}

/// An implementation block for the struct.
/// The `'_` notation indicates that the struct takes a lifetime parameter,
/// but it does not matter which, because the lifetime is not referenced in the
/// block.
impl Excerpt<'_> {
    fn level(&self) -> i32 {
        3
    }
}

/// A generic implementation block.
/// Note the lifetime parameter `'a` after `impl`.
impl<'a> Excerpt<'a> {
    fn part(&self) -> &'a str {
        self.part
    }
}

fn main() {
    // Create an instance of the `struct` using a string literal of type
    // `'static`. Therefore 'a = 'static.
    let e: Excerpt<'static> = Excerpt { part: "a part" };
    // `level` and `part` are defined for any lifetime.
    println!("{} level {}", e.part(), e.level());
    // `only_if_static` is only defined for `'static`.
    e.only_if_static();

    // Let's create another instance with a lifetime shorter than `'static`:
    let another_part = String::from("another part");
    let e2: Excerpt<'_> = Excerpt {
        part: &another_part,
    };
    // e2.only_if_static(); // ERROR: argument requires that `another_part` is
    // borrowed for `'static`
}

Avoid Self-referential Structs

std

Self-referential structs, that is structs that hold a reference to their own fields, can be tricky due to Rust's ownership and borrowing rules. You can easily run into issues when they are moved, as the references might become invalid. There is also no way to tie the lifetime of a reference to the lifetime of the struct that contains it.

Instead, you may:

  • Store only owned data in the struct,
  • Store the owned data outside the struct and let the struct hold only references,
  • Store ranges rather than references, if the pointed-to type is a sequence (array, string, vector...),
  • Use Rc or Arc.
  • Use arena-style allocation to enforce shared lifetimes.
  • Use raw pointers (Requires unsafe code).

/// 1. Avoid self-referential structs. Note how each field points to a portion
///    of the `String` owned by the struct.
struct CsvRecordTricky<'a> {
    line: String,
    fields: Vec<&'a str>,
}

/// 2. Better example of a struct that points to owned data outside of the
///    struct. Both the line and each field have the same lifetime.
#[derive(Debug)]
struct CsvRecord<'a> {
    line: &'a str,
    fields: Vec<&'a str>,
}

fn load_record<'a>(line: &'a str) -> CsvRecord<'a> {
    let mut record = CsvRecord {
        line,
        fields: Vec::new(),
    };

    record.fields = record.line.split(',').collect();
    record
}

fn outside() {
    let s = "Joe,Doe,1994-01-01".to_string();
    let c = load_record(&s);
    println!("{c:?}");
}

// 3. If multiple references to the same data are needed, `Rc<T>`
//    (single-threaded) or `Arc<T>` (multi-threaded) can be used instead.

use std::rc::Rc;

struct SelfRefRc {
    data: Rc<String>,
    reference: Rc<String>,
}

impl SelfRefRc {
    fn new(data: String) -> Self {
        let shared_data = Rc::new(data);
        SelfRefRc {
            data: Rc::clone(&shared_data),
            reference: Rc::clone(&shared_data),
        }
    }
}

fn rc() {
    let self_ref = SelfRefRc::new("Hello, Rust!".to_string());
    println!("Data: {}", self_ref.reference);
}

fn main() {
    outside();
    rc();
}

References

  • COW.
  • Memory Management.
  • Ownership & Borrowing.
  • Rust Patterns.
  • Typecasts.