1. rust
  2. /ownership
  3. /lifetimes

Lifetimes

Lifetimes ensure that references are valid for as long as they're needed. They're Rust's way of preventing dangling pointers and use-after-free errors at compile time. While often inferred, understanding lifetimes is crucial for advanced Rust programming.

What are Lifetimes?

A lifetime is the scope for which a reference is valid. Every reference in Rust has a lifetime, though it's often implicit and inferred by the compiler.

fn main() {
    let r;                // Lifetime 'a starts
    
    {
        let x = 5;        // Lifetime 'b starts
        r = &x;           // Error: x doesn't live long enough
    }                     // Lifetime 'b ends, x is dropped
    
    println!("r: {}", r); // Error: r references dropped value
}                         // Lifetime 'a ends

The problem is that r holds a reference to x, but x is dropped before r tries to use it.

Basic Lifetime Syntax

Lifetime annotations don't change how long references live - they describe the relationships between lifetimes.

Lifetime Annotation Syntax

&i32        // A reference
&'a i32     // A reference with an explicit lifetime
&'a mut i32 // A mutable reference with an explicit lifetime

Lifetime names:

  • Start with an apostrophe: 'a, 'b, 'lifetime
  • Are usually short: 'a, 'b, 'c
  • Must be declared before use

Functions with Lifetimes

When a function takes references as parameters or returns references, you often need lifetime annotations:

Problem: Ambiguous Lifetimes

// This won't compile - ambiguous lifetimes
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The compiler doesn't know whether the returned reference should live as long as x or y.

Solution: Explicit Lifetime Annotations

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

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);
    }
}

This means:

  • Both x and y must live at least as long as lifetime 'a
  • The returned reference will be valid for lifetime 'a
  • The actual lifetime is the smaller of the two input lifetimes

Lifetime Elision Rules

The compiler can often infer lifetimes using three rules:

Rule 1: Each Parameter Gets Its Own Lifetime

// Written by programmer:
fn first_word(s: &str) -> &str

// Inferred by compiler:
fn first_word<'a>(s: &'a str) -> &str

Rule 2: Single Input Lifetime is Assigned to All Outputs

// Written by programmer:
fn first_word(s: &str) -> &str

// Inferred by compiler:
fn first_word<'a>(s: &'a str) -> &'a str

Rule 3: Multiple Parameters with &self or &mut self

The lifetime of self is assigned to all output lifetime parameters:

impl<'a> ImportantExcerpt<'a> {
    // Written by programmer:
    fn announce_and_return_part(&self, announcement: &str) -> &str
    
    // Inferred by compiler:
    fn announce_and_return_part(&self, announcement: &str) -> &str // self's lifetime
}

Examples of Lifetime Annotations

Different Lifetime Patterns

// Same lifetime for all parameters and return
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Return lifetime tied to first parameter only
fn first<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

// Multiple lifetime parameters
fn compare<'a, 'b>(x: &'a str, y: &'b str) -> &'a str 
where 'b: 'a  // 'b must outlive 'a
{
    if x.len() > y.len() { x } else { x } // Can only return x
}

// No relationship between input and output lifetimes
fn make_string() -> &'static str {
    "This is a static string"
}

Functions That Don't Compile

// Error: Can't return reference to local variable
fn dangle<'a>() -> &'a str {
    let s = String::from("hello");
    &s // s is dropped at end of function
}

// Error: Can't determine which input lifetime to use
fn longest_broken<'a, 'b>(x: &'a str, y: &'b str) -> &str {
    if x.len() > y.len() { x } else { y }
}

Lifetimes in Structs

Structs can hold references, but need lifetime annotations:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
    
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part // Returns reference with same lifetime as self
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    
    println!("Important excerpt: {}", i.part);
}

Multiple Lifetimes in Structs

struct MultiRef<'a, 'b> {
    x: &'a str,
    y: &'b str,
}

fn main() {
    let string1 = String::from("long string");
    let string2 = String::from("short");
    
    let multi = MultiRef {
        x: &string1,
        y: &string2,
    };
    
    println!("x: {}, y: {}", multi.x, multi.y);
}

Advanced Lifetime Patterns

Lifetime Bounds

Specify that one lifetime must outlive another:

fn longest_with_bound<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
    'b: 'a, // 'b must outlive 'a
{
    if x.len() > y.len() {
        x
    } else {
        y // This is allowed because 'b: 'a
    }
}

Higher-Ranked Trait Bounds (HRTB)

For closures that work with any lifetime:

fn apply_to_all<F>(strings: &[String], f: F) 
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    for s in strings {
        println!("{}", f(s));
    }
}

fn main() {
    let strings = vec![
        String::from("hello"),
        String::from("world"),
    ];
    
    apply_to_all(&strings, |s| {
        if s.len() > 4 { s } else { "short" }
    });
}

Lifetime Subtyping

Longer lifetimes can be used where shorter ones are expected:

fn choose_first<'a>(first: &'a str, _second: &str) -> &'a str {
    first
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    
    {
        let string2 = String::from("xyz");
        result = choose_first(&string1, &string2);
    } // string2 dropped here
    
    println!("The result is {}", result); // Works: result has lifetime of string1
}

Static Lifetime

The 'static lifetime denotes references that live for the entire program duration:

// String literals have 'static lifetime
let s: &'static str = "I have a static lifetime.";

// Static variables
static HELLO_WORLD: &str = "Hello, world!";

fn return_static() -> &'static str {
    "This string is stored in the program binary"
}

// Box::leak can create 'static references
fn leak_example() -> &'static str {
    let string = String::from("leaked");
    Box::leak(string.into_boxed_str())
}

When to Use 'static

// Good: String literals are naturally 'static
const MESSAGE: &'static str = "Error occurred";

// Often unnecessary: lifetime elision works
fn get_message() -> &'static str {
    "Hello"
}

// Could be simplified to:
fn get_message_simple() -> &str {
    "Hello"
}

Common Lifetime Patterns

Returning References to Input

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[..]
}

fn main() {
    let my_string = String::from("hello world");
    let word = first_word(&my_string);
    println!("First word: {}", word);
}

Borrowing from Multiple Sources

struct Container<'a> {
    data: &'a str,
}

impl<'a> Container<'a> {
    fn new(data: &'a str) -> Self {
        Container { data }
    }
    
    fn get_data(&self) -> &str {
        self.data
    }
    
    // Method that borrows from self and parameter
    fn compare(&self, other: &str) -> &str {
        if self.data.len() > other.len() {
            self.data
        } else {
            // Can't return other because lifetimes don't match
            "equal or shorter"
        }
    }
}

Lifetime Bounds in Generic Functions

use std::fmt::Display;

fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Debugging Lifetime Issues

Common Error: Temporary Value Dropped

fn main() {
    let result;
    
    {
        let temp = String::from("temporary");
        result = temp.as_str(); // Error: temp doesn't live long enough
    }
    
    println!("{}", result);
}

// Solution: Move the string instead
fn main() {
    let result;
    
    {
        let temp = String::from("temporary");
        result = temp; // Move the String
    }
    
    println!("{}", result);
}

Common Error: Conflicting Lifetimes

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

// Solution: Use different lifetimes if they don't need to be the same
fn better<'a, 'b>(x: &'a str, y: &'b str) -> (&'a str, &'b str) {
    (x, y)
}

Lifetime Elision in Practice

Cases Where Annotations Are Required

// Multiple inputs, unclear which lifetime to use for output
fn needs_annotation<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

// Multiple inputs, returning both
fn needs_annotation2<'a>(x: &'a str, y: &'a str) -> (&'a str, &'a str) {
    (x, y)
}

Cases Where Annotations Are Optional

// Single input: lifetime is clear
fn no_annotation_needed(s: &str) -> &str {
    &s[1..]
}

// Methods: self's lifetime is used
impl<'a> Container<'a> {
    fn get(&self) -> &str {
        self.data
    }
}

Best Practices

1. Start Without Lifetime Annotations

Let the compiler tell you when they're needed:

// Start with this
fn first_word(s: &str) -> &str {
    // Implementation
}

// Add annotations only if compiler requires them
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // Implementation
}

2. Use Descriptive Lifetime Names for Complex Cases

// For simple cases, 'a is fine
fn simple<'a>(x: &'a str) -> &'a str { x }

// For complex cases, use descriptive names
fn complex<'input, 'output>(
    data: &'input str,
    cache: &mut HashMap<&'input str, &'output str>
) -> &'output str {
    // Implementation
}

3. Prefer Owned Types When Lifetimes Get Complex

// Instead of complex lifetime relationships
fn complex_lifetimes<'a, 'b>(x: &'a str, y: &'b str) -> SomeStruct<'a, 'b> {
    // Complex implementation
}

// Consider using owned types
fn simpler(x: String, y: String) -> SomeOwnedStruct {
    // Simpler implementation
}

4. Use Lifetime Bounds Sparingly

// Only when necessary
fn with_bound<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
    'b: 'a,
{
    if x.len() > y.len() { x } else { y }
}

Lifetimes ensure memory safety by preventing dangling references at compile time. While they can seem complex, the compiler's error messages and lifetime elision rules make them manageable in most cases. Focus on understanding the relationships between references rather than memorizing syntax rules.