CS3984 Computer Systems in Rust



Lifetimes

  1. Every value in Rust has a lifetime.
  2. The lifetime of a value starts when the value is initialized and ends when the value is freed.
  3. The lifetime of a value is also called the value’s scope.
fn main() {
    let a: i32 = 5;              // -------------+-- 'a
                                 //              |
    {                            //              |
        let b: i32 = 6;          // =====+== 'b  |
        println!("value: {b}");  //     ||       |
    }                            // =====+       |
                                 //              |
    println!("value: {a}");      //              |
}                                // -------------+


Lifetimes (2)

  1. Every reference in Rust has a lifetime.
  2. The lifetime of a reference starts when the reference is initialized and ends when the reference is no longer used.
fn main() {
    let value: i32 = 5;
    let reference: &i32 = &value;       // ----------------+-- 'r
                                        //                 |
    println!("reference: {reference}"); // ----------------+
}


Lifetimes (3)

  1. The lifetime of a reference cannot be greater than the lifetime of the value it refers to (referents must outlive their references).
fn main() {
    let value: i32 = 5;                 // ------------------+-- 'v
    let reference: &i32 = &value;       // =========+== 'r   |
                                        //         ||        |
    println!("reference: {reference}"); // =========+        |
}                                       // ------------------+


Lifetime Error

fn main() {
    let reference: &i32;

    {
        let value: i32 = 5;             // --------+-- 'v
        reference = &value;             // ========|=======+== 'r
    }                                   // --------+      ||
                                        //                ||
    println!("reference: {reference}"); // ================+
}


Lifetimes (4)

fn main() {
    let value_1: i32 = 5;                    // --------------------------+-- 'v1
    let mut reference: &i32;                 //                           |
                                             //                           |
    {                                        //                           |
        let value_2: i32 = 100;              // -----------------+-- 'v2  |
        reference = &value;                  // =====+== 'r(v2)  |        |
                                             //     ||           |        |
        println!("reference: {reference}");  // =====+           |        |
    }                                        // -----------------+        |
                                             //                           |
    reference = &value_1;                    // ===========+== 'r(v1)     |
                                             //           ||              |
    println!("reference: {reference}");      // ===========+              |
}                                            // --------------------------+


Lifetimes and Aliasing (1)

fn main() {
    let mut x = 5;              // ---------------------+-- 'x1
                                //                      |
    let r = &x;                 // ======+== 'r1(x1)    |
                                //      ||              |
    x = 100;                    // -----||--------------+-- 'x1*
                                //      ||              |
    println!("{} {}", x, *r);   // ======+--------------+
}


Lifetimes and Aliasing (2)

fn main() {
    let mut x = 5;              // ---------------------+-- 'x1
                                //                      |
    let r = &x;                 // ========= 'r1(x1)    |
                                //                      |
    x = 100;                    // ---------------------+-- 'x1*
                                //                      |
    println!("{}", x);          // ---------------------+
}


Lifetimes and Functions

Q: What is the lifetime of the reference returned?

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


Lifetimes Annotations

We can explicitly annotate the lifetime of references in function parameters.

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


This means that for some lifetime 'a:

  • The references x and y live at least as long as 'a
  • The returned reference lives for at least as long as 'a

Lifetimes Annotations (2)

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

fn main() {
    let s1 = String::from("crab");          // ----------------------------+-- 's1
    let s2 = String::from("penguin");       // --------------------+-- 's2 |
    let result = longest(&s1, &s2);         // ===+== 'r ('s1|'s2) |       |
                                            //   ||                |       |
    println!("{result}");                   // ===+                |       |
}                                           // --------------------+-------+


Lifetimes Annotations (3)

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

fn main() {
    let s1 = String::from("crab");          // ----------------------------+-- 's1
    let result: &str;                       //                             |
    {                                       //                             |
        let s2 = String::from("penguin");   // --------------------+-- 's2 |
        result = longest(&s1, &s2);         // ===+== 'r ('s1|'s2) |       |
    }                                       // --||----------------+       |
    println!("{result}");                   // ===+                        |
}                                           // ----------------------------+


Lifetimes Annotations in Struct Definitions

struct ArrayPart<'a> {
    part: &'a [i32],
}

fn main() {                                     //                              'a
    let array: [i32; 5] = [1, 2, 3, 4, 5];      // -----------------------------|
                                                //                              |
    let slice: &[i32] = &array[1, 2, 3];        // =================+== 's ('a) |
                                                //                 ||           |
    let a_part = ArrayPart { part: slice };     // ===+== 'ap ('a) ||           |
                                                //   ||            ||           |
    println!("{:?}", slice);                    // ==||=============+           |
    println!("{:?}", a_part.part);              // ===+                         |
}                                               // -----------------------------+


Lifetime Elision

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


is equivalent to…

fn first_word<'a>(s: &'a str) -> &'a str {
    // ...
}


Lifetime Elision Rules

  1. Each function parameter that is a reference gets its own lifetime parameter.
fn func(x: &i32) {}

fn func2(x: &i32, y: &i32) {}


fn func<'a>(x: &'a i32) {}

fn func2<'a, 'b>(x: &'a i32, y: &'b i32) {}


  1. If there is exactly one input lifetime parameter, the lifetime is assigned to all output lifetime parameters.
fn func(x: &i32) -> (&i32, &i32) {}


fn func<'a>(x: &'a i32) -> (&'a i32, &'a i32) {}


Lifetime Elision Rules (2)

  1. 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.
struct NumWrapper { num: i32 }

impl NumWrapper {
    fn observe_other_return_self(&self, other: &i32) -> &i32 {
        println!("Observing: {other}");
        &self.num
    }
}


is equivalent to

    fn observe_other_return_self<'a, 'b>(&'a self, other: &'b i32) -> &'a i32 {
        // ...
    }


Lifetime Elision Rules (3)

  1. 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.
struct NumWrapper { num: i32 }

impl NumWrapper {
    fn observe_self_return_other(&self, other: &i32) -> &i32 {
        println!("Observing: {}", self.num);
        other
    }
}


Lifetime Elision Rules (4)

  1. 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.
struct NumWrapper { num: i32 }

impl NumWrapper {
    fn observe_self_return_other<'o>(&self, other: &'o i32) -> &'o i32 {
        println!("Observing: {}", self.num);
        other
    }
}