Lifetimes

Now you know about ownership and borrowing, but there's still more attached to it, namely lifetimes. Those can (and are) elided in almost all cases, so knowing about the semantics isn't strictly necessary when working with normal code. However, understanding lifetimes can still be useful, especially when working with more complicated stuff or multiple references to the same item.

Basically, as items can't be moved or mutated while a reference points to them, they also can't go out of scope and therefore be cleaned up. This means that references have to guarantee they don't outlive the data they're pointing to.


#![allow(unused)]
fn main() {
let reference;

// new, smaller scope
{
  let answer = 42;
  reference = &answer;
  // answer goes out of scope here and would leave
  // the reference pointing to invalid memory!
}

println!("{}", *reference);
}

Lifetimes get assigned automatically as long as deducting the correct one is trivial. In more complicated cases, you can assign then yourself just like a generic parameter. Here's an example of that:

struct ObjectWithReference<'a> {
  pub reference: &'a usize
}

The meaning of a lifetime parameter changes depending on where it shows up. As a generic parameter on a type, it means that an object of the type has some lifetime, assigned by the compiler and deducted from the scope it's in. On reference fields inside a type, it means that the item the reference is pointing to must live at least as long as the assigned lifetime. In the example, it means that a ObjectWithReference object must not outlive whatever reference is pointing to. Let's look at the usage of it.


#![allow(unused)]
fn main() {
struct ObjectWithReference<'a> { pub reference: &'a usize }

let mut outer_value = 42;
// okay, same scope, same lifetime
let outer_reference = ObjectWithReference {
  reference: &mut outer_value
};
let outliving_reference;

// new, smaller scope
{
  let mut inner_value = 69;
  // okay, value pointed to outlives the reference
  let inner_reference = ObjectWithReference {
    reference: &mut outer_value
  };
  // NOT okay, outlives inner_value!
  outliving_reference = ObjectWithReference {
    reference: &mut inner_value
  };
}

println!("{}", *outliving_reference.reference);
}

Next, we're gonna look at functions with lifetimes. I'm taking the example straight out of the official Rust book.

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

This is also using the provided lifetime with different meanings. On input parameters, it constraints the lifetime to the shortest lifetime of any input parameter using that lifetime. On an output parameter, it constraints the output to the lifetime. In total, it means that the output is not allowed to outlive any of the input parameters. I'm gonna take the usage bit from the book too, but I will annotate it a bit:


#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }

fn example() {
  let string1 = String::from("long string is long");
  let result;
  
  // new, smaller scope
  {
    let string2 = String::from("xyz");
    // references from String::as_str have the same lifetime
    // as the String object
    result = longest(string1.as_str(), string2.as_str());
    
    // string2 goes out of scope here, but result musn't
    // outlive any of its inputs, so this will not compile!
  }
  
  println!("The longest string is {}", result);
}
}

As you can see, the rules don't care about runtime behaviour, since they are applied at compile time. At runtime, string2 would never be returned, therefore result would never point to invalid memory.

To avoid excessive scoping and quite a bit of headaches, Rust has the ability to shorten the lifetime of a reference to its last usage. It means any of the examples in this lifetime subchapter will compile if you just delete the last print statement, since the lifetimes of any outliving reference can be shortened to its last usage, which is its assignment. I will also throw in another more explicit example:


#![allow(unused)]
fn main() {
let mut answer = 42;
let reference = &answer;

// last usage of the reference
println!("{}", reference);

// Is okay since the first reference can
// be dropped before it
let mutable_reference = &mut answer;
*mutable_reference = 1;
println!("{}", mutable_reference);
}

One last thing that deserves a mention is the special 'static lifetime. It's the lifetime of the program itself, so there are no constraints referencing items with that lifetime. However, normal borrowing rules still apply, of course. It can be useful when working with threads or manually implementing shared ownership with unsafe code, but the most notable use of 'static lifetimes are string literals:

let message: &'static str = "Hello, world!";