Rust Lifetime Parameters

When a programming language makes you whip out a dictionary

Dictionary entry in book

A weird word is about to help me learn Rust lifetime parameters and I’ll start to piece together when they are needed. This is a complicated part of Rust tracking ownership and is somewhat unique, at least in the languages I’ve been exposed to up to this point.

Elision

e·li·sion /əˈliZHən/
1. The omission of a sound or syllable when speaking (as in I’m, let’s)
2. The process of joining together or merging things, especially abstract ideas.

So, I’m guessing definition 2 is why we have this in Rust, and definition 1 explains how the syntax developed. Anyway, I’m trying to piece it together. This is also referred to as a lifetime parameter in Rust. But what is it? Why do we does the compiler need it? What’s it for?

BTW, Rust 1.36.0 came out today! Congratulations Rust team!! Be sure to update!!

Help With Lifetimes

From The Rustonomicon, the advanced book of Rust for when you get bored, “Lifetimes are named regions of code that a reference must be valid for. … In most of our examples, the lifetimes will coincide with scopes. This is because our examples are simple.”[link] So, let’s look at some examples to figure this out:

pub struct Vehicle {
    make: String,
    model: String,
}

fn main() {
    let mut garage: Vec<Vehicle> = Vec::new();

    garage.push(Vehicle {
        make: "Subaru".to_string(),
        model: "Crosstrek".to_string(),
    });

    for vehicle in garage {
        println!("I have a {} {}", vehicle.make, vehicle.model);
    }
}

Here, we’ve created a simple struct to hold a Vehicle – with its make and model as two Strings. This compiles and runs just fine (and there are lifetimes involved, the compiler is figuring them all out for us.)





Hrm, but a vehicle’s make and model don’t change overnight (this isn’t a Pixar movie)… if we change them from String (the heap storage variable that can adapt in size as we change the value) into a str (fixed-length, generally immutable) type, maybe that’s better!

pub struct Vehicle {
    make: str,
    model: str,
}

but, oh, I have a bug

Bug with droplets of water crawling on plant
You’re still here? It’s over. Go home. Go.
  |
9 |     make: str,
  |     ^^^^^^^^^ doesn't have a size known at compile-time
  |

oh, right…. they have to be initialized at compile time – so that would only work as a constant. Ok, then, when we initialize with the strings, we’ll let the struct borrow our strings, that’s why you almost always see &str in code:

pub struct Vehicle {
    make: &str,
    model: &str,
}

and here we come to it:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:11
  |
9 |     make: &str,
  |           ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
  --> src/main.rs:10:12
   |
10 |     model: &str,
   |            ^ expected lifetime parameter
Smaller version, same bug, same plant
Yup, another one…

rustc is telling us it cannot enforce the default lifetime for these fields inside the struct, because their lifetime is not affixed to the lifetime of the struct instance, but the struct instance depends on those fields!

The compiler is having a fit because when we initialize the field make with “Subaru”, the struct borrows a reference to that string. Which means if that string goes out of scope (and gets freed by the compiler) before the struct does, the struct would then be pointing out in no-man’s-land, which is bad! The compiler knows it can’t control this, so it bails.

Fixing things

pub struct Vehicle<'a> {  
    make: &'a str,  
    model: &'a str,  
}

The ‘a (pronounced “tic a”) is explaining to the compiler how it should handle the lifetimes.

We are basically adding a parameter to the definition – we used ‘a but we could have used ‘w or ‘lifetime (though, I’ve never seen it other than a single character, usually starting at ‘a for the first one you need). With this parameter, we are saying to the compiler:

when you construct an instance of this type, let’s agree to call the instance’s lifetime (that you manage) by the variable a and you let me know what that lifetime is … AND btw… when you borrow the strings for make and model as references, I’m telling you to make sure their lifetime is whatever a is. In other words, make sure that the memory for make and model hangs around for exactly as long as the struct instance itself does.”

This now looks just a little more complicated because we had to give hints to the compiler of how to maintain lifetimes, but runs just fine again:

pub struct Vehicle<'a> {
    make: &'a str,
    model: &'a str,
}

fn main() {
    let mut garage: Vec<Vehicle> = Vec::new();

    garage.push(Vehicle {
        make: "Subaru",
        model: "Crosstrek",
    });

    for vehicle in garage {
        println!("I have a {} {}", vehicle.make, vehicle.model);
    }
}

Author: Jeff Culverhouse

I am a remote Sr Software Engineer for ZipRecruiter.com, mainly perl. Learning Rust in my spare time. Plus taking classes at James Madison University. Culverhouse - English: from Old English culfrehūs ‘dovecote’, hence a topographic name for someone living near a dovecote, or possibly a metonymic occupational name for the keeper of a dovecote. ISTP, occasionally INTP.