Lifetime

A lifetime is a construct in Rust that represents the scope of a reference. The purpose of lifetimes is to ensure that references are always valid or to ensure a reference doesn't outlive its referent.

Lifetimes are entirely figments of Rust's compile-time imagination. At runtime, a reference is nothing but an address; its lifetime is part of its type and has no runtime representation. -- The Programming Rust book

The main aim of lifetimes is to prevent dangling references. -- The Book

Lifetime annotation ('a pronounce tick a)

&i32 // a reference without lifetime annotation
&'a i32 // a reference with explicit lifetime annotation
&'a mut i32 // a mutable reference with explicit lifetime annotation

Lifetime elision rules

Lifetime elision rules are a set of rules in Rust that allow the compiler to infer lifetimes in certain cases, without the need for explicit annotations.

  • First Rule: Rust assigns a different lifetime paramater to each lifetime in each input type.
    • fn foo(x : &i32) becomes fn foo<'a>(x : &'a i32)
    • fn foo(x : &i32, y : &i32) becomes fn foo<'a, 'b>(x : &'a i32, y : &'b i32)
    • fn foo(x : &ImportantExcerpt) becomes fn foo<'a>(x : &'a ImportantExcerpt)
  • Second Rule: If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32
  • Third Rule: If there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters.

Lifetimes on functions

Dangling reference example

// 'b is smaller than 'a and Rust rejects the program
fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

The following functions return dangling references and won't compile.


#![allow(unused)]
fn main() {
fn longest(fst : &str, snd: &str) -> &str {
    let string = "hello";
    return string.as_str();
}

// the returning lifetime is not related to the lifetime of paramaters
fn longest_2<'a>(fst : &str, snd: &str) -> &'a str {
    let string = "hello";
    return string.as_str();
}
}

Passing references and returning a reference from function

Lifetime annotations need to be explicitly provided if Rust cannot infer lifetimes for input or output paramaters.

fn max<'a>(a : &'a i32, b : &'a i32) -> &'a i32 {
    if *a > *b {
        a
    } else {
        b
    }
}

// this is OK 
fn max<'a>(a : &'a i32, b : &i32) -> &'a i32 {
    a
}

// dangling pointer case here. won't compile
fn max_inner(a : &i32) -> &i32 {
    let b = 5;
    max(&a, &b)
}

fn main() {
    let x = 10;
    let y = 20;
    let result = max(&x, &y);
    println!("{result}");
   
    // occurs dangling pointer and won't compile
    let result = max_inner(&x);
    println!("{result}");
}

Lifetime with mutable references Example

2 lifetime annotations - one for mutable referenced container and one for the shared value - must be explicitly provided in the following example.

fn insert_str<'c, 'v>(source: &'c mut String, s : &'v str) {
    source.push_str(s);
}

fn insert_num<'c, 'v>(nums : &'c mut Vec<&'v i32>, num : &'v i32) {
    nums.push(num);
}

fn main() {
    let mut source = String::new();
    insert_str(&mut source, "hello");
    insert_str(&mut source, " world");
    println!("{:?}", source);

    let mut nums = Vec::new();
    insert_num(&mut nums, &10);
    insert_num(&mut nums, &11);
    println!("{:?}", nums);
}

Lifetimes on Types

Whenever a reference type appears inside another type's definition, you must write out its lifetime.

#[derive(Debug)]
struct FirstLast<'a> {
    first: &'a i32,
    second: &'a i32,
}

// no need to explicitly annotate lifetimes here 
// due to the first and second rule of lifetime elision rules
fn get_first_last(source: &[i32]) -> FirstLast {
    FirstLast {
        first : &source[0],
        second: &source[source.len() - 1],
    }
}

fn main() {
   let nums = vec![3,4,5,2,3,4,1,-2];
   let fl = get_first_last(&nums); 
   println!("{fl:?}");
}

Lifetime annotation on Enums.


#![allow(unused)]
fn main() {
enum MaybeString<'a> {
    Maybe(&'a str),
    Nothing
} 
}

Lifetimes on Method definition

#[derive(Debug)]
struct Excerpt<'a> {
    part : &'a str
}

impl<'a> Excerpt<'a> {
    fn new(part : &'a str) -> Self {
       Excerpt { part } 
    }

    fn display_and_return_part(&self) -> &str {
        println!("{self:?}");
        self.part
    }
}

fn main() {
    let s = "hello world";
    let expt = Excerpt::new(s.split(' ').next().unwrap());
    expt.display_and_return_part();
}

The Static Lifetime ('static)


#![allow(unused)]
fn main() {
let s : &'static str = "I'm static string"
}

I'm static string is stored directly in the program's binary which is located in static memory region.