February 2, 2024   -   David Oyinbo

Overview of Lifetimes in Rust: A Guide for Developers

Time catches up to all of us, even references. Rust's lifetime system is central to its memory management approach, ensuring references don't outlive the data they point to without relying on a garbage collector.

RustLifetimesMemory SafetyProgrammingSystems

Overview

Understanding lifetimes is essential for writing safe and efficient Rust code. This guide explores how lifetimes work in Rust, from basic concepts to advanced scenarios including function signatures, structs, static lifetimes, and the borrow checker. Whether you're new to Rust or looking to deepen your understanding, this guide will help you master one of Rust's most important features.

What are Lifetimes?

In Rust, lifetimes are a way to track how long references to data are valid and to ensure references don't outlive the data they point to. A variable's lifetime begins when it is created and ends when it is destroyed. In other words, lifetimes help the Rust compiler ensure that your code is memory-safe.

Imagine you have a reference to a piece of data, such as a string slice (&str) or a reference to a struct. The lifetime of that reference defines how long it is safe to use that reference. If you attempt to use the reference outside of its valid lifetime, the Rust compiler will generate a compilation error.

Lifetimes are indicated by single-letter names, conventionally 'a, 'b, etc., and they are often associated with function signatures, data structures, and references.

Anatomy of Lifetimes

Let's take an example:

main.rsrust
fn longest(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = "This is string slice one";
    let s2 = "This is string 2 slice";
    let l = longest(s1, s2);
    println!("Longest string: {}", l);
}

This should work but we get a complaint from the compiler because it doesn't know how long the returned reference should live. The compiler needs explicit lifetime annotations to understand the relationship between the input and output lifetimes.

Function Signatures

In Rust, function signatures often include lifetime annotations to specify the relationship between the lifetimes of function parameters and return values. For example:

main.rsrust
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

In this function, the lifetime annotation 'a indicates that both s1 and s2 must have the same lifetime, and the return value of the function will also have the same lifetime 'a. This ensures that the returned reference doesn't outlive the references passed as arguments.

Lifetime Coercion

In a case where s1 and s2 have different lifetimes and you want to ensure that the returned reference would not outlive either of them and coerce the longer lifetime into a shorter one, then you would need to return a reference with the lifetime that is the smallest of the s1 and s2:

main.rsrust
// `<'a: 'b, 'b>` reads as lifetime `'a` is at least as long as `'b`.
// Here, we take in an `&'a i32` and return a `&'b i32` as a result of coercion.
fn longest<'a: 'b, 'b>(s1: &'a str, s2: &'b str) -> &'b str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

This is called coercion. Note that Rust can infer lifetimes as given below:

main.rsrust
// Here, Rust infers a lifetime that is as short as possible.
// The two references are then coerced to that lifetime.
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

Structs and Data Ownership

Structs in Rust can contain references with different lifetimes. For example:

main.rsrust
struct Book<'a> {
    title: &'a str,
    author: &'a str,
}

fn main() {
    let title = String::from("The Rust Programming Language");
    let author = String::from("Steve Klabnik and Carol Nichols");
    let book: Book = Book {
        title: &title,
        author: &author,
    };
    
    println!("Book: {} by {}", book.title, book.author);
}

In this example, the Book struct has two fields with the same lifetime 'a, indicating that the references to title and author must have the same lifetime as the Book instance.

Static Lifetime

The 'static lifetime is a special lifetime that represents the entire duration of the program. References with the 'static lifetime can be used anywhere and never become invalid. For example:

main.rsrust
fn main() {
    let message: &'static str = "Hello, World!";
    println!("{}", message);
}

And also consider this:

main.rsrust
fn new_string_slice() -> &str {
    "Something is here"
}

fn main() {
    let s = new_string_slice();
    println!("New String: {}", s);
}

This wouldn't compile as the compiler doesn't know how long the string slice returned would live. This could be solved using these two approaches:

main.rsrust
// Approach 1: Generic lifetime parameter
fn new_string_slice<'a>() -> &'a str {
    "Something is here"
}

// Approach 2: Static lifetime
fn new_string_slice() -> &'static str {
    "Something is here"
}

message and the returned string slice from new_string_slice has the 'static lifetime because it refers to a string literal that exists for the entire program's duration.

Lifetimes and Borrow Checker

The Rust compiler employs a borrow checker that uses lifetime annotations to ensure that references are used correctly. The borrow checker enforces the following rules:

  1. References must have a valid lifetime - Every reference must be valid for the duration it's used
  2. References must not outlive the data they point to - A reference cannot exist longer than the data it references
  3. Mutable references cannot coexist with immutable references - &mut references cannot coexist with & references to the same data

By adhering to these rules, Rust guarantees memory safety without the need for a garbage collector.

Lifetime Elision

Rust includes a set of lifetime elision rules that allow you to omit explicit lifetime annotations in many cases, making code cleaner and easier to read. These rules are applied automatically by the compiler. For example, in function signatures, you can often omit lifetime annotations, and Rust will infer them based on the function's input and output parameters.

The three main elision rules are:

  1. Input lifetime elision: Each parameter gets its own lifetime
  2. Single input lifetime: If there's one input lifetime, it's assigned to all output lifetimes
  3. Method receiver lifetime: In methods, the lifetime of &self or &mut self is assigned to all output lifetimes

Conclusion

Understanding lifetimes is essential for writing safe and efficient Rust code. While they may initially seem complex, they are a powerful tool for preventing memory-related bugs. By adhering to Rust's lifetime rules and using lifetime annotations effectively, you can harness the full potential of Rust's memory safety guarantees and write reliable and performant code. As you continue to work with Rust, lifetimes will become an integral part of your programming toolkit, enabling you to build robust and secure software systems.


Originally published on Medium on February 2, 2024.

Related Logs

February 2, 2024

Overview of The Proxy Design Pattern

A simplified overview of the proxy design pattern with examples in Java - exploring structural design patterns that provide surrogates or placeholders for other objects

design-patternssoftware-developmentcoding
View log
February 2, 2024

Error Handling in Rust Where Bugs Go to Take a Vacation

Safeguarding Software Reliability with Rust's Error Handling Mechanisms - exploring Result and Option enums, the ? operator, custom error types, and panic! for robust error management

rusterror-handlingprogramming
View log
April 21, 2024

Understanding the Client-Server Model in Distributed Computing

Exploring the fundamental architecture in distributed computing that facilitates efficient allocation of tasks and workloads between clients and servers through network communication

distributed-computingclient-servernetworking
View log

Let's build something together

Available for senior engineering roles, consulting, and architecture reviews.

© 2025 David Oyinbo