This is the third part of the Learn You a Rust for Great Good! tutorial explainer series. If you’re coming to the series for the first time, I recommend starting at the first post linked above.

I’m back! Sorry for the month of hiatus - been very busy [citation needed] working on my personal audio project (more on that later!) and having a bit too much fun with OpenTTD. Anyway, without further ado…

EDIT I (18/4): Changed a few incorrect assumptions, with help from people on /r/rust.

Last time, we discussed the concept of borrowing - lending out our owned objects to functions and suchlike - to make the idea of ownership less painful. We had a few examples of terrible, shameful code that violated these rules, and explained why the code was so terrible and shameful.

This time, we will cover a concept that has befuddled many new Rustaceans (including me!): that of the dreaded lifetime.

Newbie vs. Lifetimes: Round I

Most Rustaceans encounter lifetimes for the first time via two ways. Let’s have a look at these.

Problem One: Storing a borrow in a struct

So you’ve just heard about borrows. They’re pretty nice; they make ownership easier. In particular, they let us write code like this:

struct Object {
    number: u32
}

struct Multiplier {
    object: &Object,
    mult: u32
}

fn print_borrower_number(mu: Multiplier) {
    println!("Result: {}", mu.object.number * mu.mult);
}

fn main() {
    let obj = Object { number: 5 };
    let obj_times_3 = Multiplier { object: &obj, mult: 3 };
    print_borrower_number(obj_times_3);
}

The Multiplier is a special object that provides a way to represent an object multiplied by a number without having to touch the original object at all - something we couldn’t do with ownership (well, not without cloning the original object). Cool, right?

Unfortunately for us, this code will fail to compile, spitting out some weird lifetime error.

multiplier.rs:6:13: 6:20 error: missing lifetime specifier [E0106]
multiplier.rs:6     object: &Object,
                            ^~~~~~~
multiplier.rs:6:13: 6:20 help: run `rustc --explain E0106` to see a detailed explanation

Running the --explain gives us this verbage:

This error indicates that a lifetime is missing from a type. If it is an error
inside a function signature, the problem may be with failing to adhere to the
lifetime elision rules (see below).

Strange. Let’s have a look at the other common lifetime problem, and then I’ll reveal what the hell is going on here.

Problem Two: Borrowed types in a function

Let’s use the power of borrowing to write another handy function: this time, to add an Object to another mutable Object, without having to transfer ownership.

fn object_combinator(a: &mut Object, b: &Object) -> &mut Object {
    a.number = a.number + b.number;
    a
}

fn main() {
   let mut a = Object { number: 3 };
   let b = Object { number: 4 };
   println!("Result: {}", object_combinator(&mut a, &b).number);
}

This gives us the same error as above. What the hell, Rust?!

multiplier.rs:10:53: 10:64 error: missing lifetime specifier [E0106]
multiplier.rs:10 fn object_combinator(a: &mut Object, b: &Object) -> &mut Object {
                                                                     ^~~~~~~~~~~
multiplier.rs:10:53: 10:64 help: run `rustc --explain E0106` to see a detailed explanation

Birth of a Lifetime

Let’s take a step back from all these errors. There we go - clear your head. I’m now going to explain why lifetimes exist, and why they’ve come into our Rust programming careers just when we were starting to have some fun.

When we looked at borrows beforehand, we set down some ground rules about what you can and can’t do with them. Here they are:

  1. Any borrow must last for a scope no greater than that of the owner. (if I take away your object while you’re trying to use it, you’ll try to use it anyway and end up possibly using something else)
  2. You may have either 1+ immutable borrow(s) or exactly 1 mutable borrow at a time - never both. (if I edit your object while you’re trying to use it, you’ll get REALLY confused and bad things will happen)

The thing is, how is the compiler supposed to check if we’re abiding by these rules? Lifetimes, unsurprisingly, are the answer.

Lifetimes are used simply to describe how long objects live for. When you make a new variable, the compiler attaches a lifetime to it. Then, by comparing all of the lifetimes to each other, the compiler can know whether your code is correct.

To quote the Rustonomicon:

Rust enforces [the borrowing rules] through lifetimes. Lifetimes are effectively just names for scopes somewhere in the program. Each reference, and anything that contains a reference, is tagged with a lifetime specifying the scope it's valid for.

Remember that variables go away when your code leaves their scope (the function or code block where they were created) - in the opposite order that they were created (unless, of course, they are moved out of that scope). Look at this example:

// This is an object which owns a borrow to something,
// rather like our `Multiplier` from before.
// You can ignore the strange <'x> notation for now -
// don't worry, I'll cover it later.
struct RefObject<'x>(&'x u32);

fn steal_a_var<'x>(o: RefObject<'x>) {
    println!("{}", o.0);
}

fn main() {
    // a is created in main()s scope
    let a = 3;
    
    // b is created in main()s scope
    let mut b = &a;
    
    // c is created in main()s scope
    let c = RefObject(&b);
    
    // c is moved out of main()s scope.
    // c now lives as long as steal_a_var()s scope.
    steal_a_var(c);
    
    // steal_a_var()s scope ends, killing all the variables inside it...
    // c goes away
    
    // d is created in main()s scope
    let d = &mut b;
    
}
// main()s scope ends, killing all the variables inside it...
// d goes away, as it was declared last
// b goes away, as it was declared second-last
// a goes away, as it was declared third-last

What’s actually going on here is a little more nuanced than the above example, actually. Get this: when a variable is created, it creates its own little mini-scope that lasts for as long as it is needed lasts until the scope containing it ends. For example, d goes away first because its mini-scope ends before b’s does. Let me rewrite this example, making the scopes more obvious:

fn main() {
    // The `'a: {}` syntax used here isn't actually valid, I'm just using it
    // to show you the scopes in this program.
    'a: {
        // a is created, it gets its own scope 'a -
        // lasting as long as the scope containing it (main()'s scope)
        let a = 3;
        'b: {
            // b is created, it gets its own scope 'b -
            // lasting as long as the scope containing it
            let mut b = &a;
            'c: {
                // c is created, it gets its own scope 'c -
                // only lasting until steal_a_var(), as it is moved out
                let c = RefObject(&b);
                steal_a_var(c);
            } // c goes away
            'd: {
                // d is created, it gets its own scope 'd -
                // lasting as long as the scope containing it
                let d = &mut b;
            } // d goes away
        } // b goes away
    } // a goes away
}

Rust now looks at all the lifetimes:

  • it sees that 'a is bigger than 'c and 'd, and knows that there are no borrows lasting longer than what they borrow
  • it sees that 'c and 'd are separate from each other, and knows that the rules regarding mutable borrows are not broken

Phew. Wasn’t that painful? Luckily, Rust automatically creates and manages these mini-scopes for us, so we never have to write them in and our code will just work in 99.9% of cases.

However, sometimes Rust can’t (or won’t, due to its philosophy of being explicit) fill these scopes in for us. Sometimes, it needs more information. That’s when we have to pencil some information about these scopes in for Rust to understand what we’re on about.

Fixing Our Problems

Right. Now we at least know what lifetimes are, I can show you how we use them to solve our problems.

Problem One: Storing a borrow in a struct

struct Object {
    number: u32
}

struct Multiplier {
    object: &Object,
    mult: u32
}

Recall that this code was giving us the following error:

multiplier.rs:6:13: 6:20 error: missing lifetime specifier [E0106]
multiplier.rs:6     object: &Object,
                            ^~~~~~~
multiplier.rs:6:13: 6:20 help: run `rustc --explain E0106` to see a detailed explanation

Rust is complaining about not being able to figure your lifetimes out for you. Let’s step through the logic.

  • We introduce an Object type, containing a single number. Rust is happy with this idea, as it figures that the number will live as long as the Object does.
  • We introduce a Multiplier type, containing a mult and a borrow of an Object. Rust is happy about the mult - it uses the same logic as above - but it doesn’t like the object borrow.
  • The thing is, object could live for any odd lifetime - possibly one smaller than the Multiplier’s! Rust wants us to constrain the Multiplier so that it can check your code.

Here’s the solution:

struct Multiplier<'a> {
    object: &'a Object,
    mult: u32
}

Argh! New, complicated syntax! What is this madness?!

Basically, this code says “I have an object here called Multiplier that lives as long as the lifetime 'a. Given that lifetime, I have an object inside me that lasts as least as long as 'a.” (The less-than-greater-than bracket syntax is Rust’s way of specifying generics - you can read <'a> as “for a given lifetime 'a”)

This then “links” the lifetime of a Multiplier to the lifetime of the Object inside it, making sure the Object won’t go away before the Multiplier does. The compiler is now happy with our definition of a Multiplier, and the code will compile.

(This is also how the RefObject we created whilst explaining mini-scopes gets to compile.)

Problem Two: Borrowed types in a function

fn object_combinator(a: &mut Object, b: &Object) -> &mut Object {
    a.number = a.number + b.number;
    a
}

fn main() {
   let mut a = Object { number: 3 };
   let b = Object { number: 4 };
   println!("Result: {}", object_combinator(&mut a, &b).number);
}

Rust doesn’t like our object_combinator here.

multiplier.rs:10:53: 10:64 error: missing lifetime specifier [E0106]
multiplier.rs:10 fn object_combinator(a: &mut Object, b: &Object) -> &mut Object {
                                                                     ^~~~~~~~~~~
multiplier.rs:10:53: 10:64 help: run `rustc --explain E0106` to see a detailed explanation

Thankfully, the people behind Rust are helpful and kind people who put in a little tip for us confused programmers.

multiplier.rs:10:53: 10:64 help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`

The problem, as described above, is simple: you’re giving object_combinator two borrows with possibly different lifetimes, and returning one borrow. How is Rust supposed to figure out how long the borrow you returned lives for? It can’t just guess for you! Rust squawks at us and asks us to help it figure out what’s going on.

We know that our object_combinator adds the second Object (b)’s number to a, and then returns a. Therefore, it’s a’s lifetime we care about here, as that’s what we’re returning. Let’s annotate our function with lifetimes to clue Rust in.

fn object_combinator<'a, 'b>(a: &'a mut Object, b: &'b Object) -> &'a mut Object {
    a.number = a.number + b.number;
    a
}

This is saying to Rust: “I have an object_combinator which takes in a borrow to an object that lives as long as the lifetime 'a, and one that lives as long as the lifetime 'b. I’m going to return a borrow to an object that lives as long as the lifetime 'a.” Rust is happy with this definition, and lets our code compile.

‘static

The 'static lifetime is a special lifetime which means “lives for the entire program”. You’ll probably see it first when dealing with strings:

let string: &'static str = "I'm a string! Yay!";

and, unsurprisingly, static things:

static UNIVERSE_ANSWER: u32 = 42;

let answer_borrow: &'static u32 = &UNIVERSE_ANSWER;

You’ll also see it when trying to program things that are multithreaded. Sending borrows to things that live for a lifetime that isn’t 'static to another thread is disallowed, because it’s possible for the thread you’re sending something to to outlive the thread you sent it from - invalidating the reference.

Lifetime Elision

Lifetime elision is an example of Rust filling things in for you to make programming less unbearable - and it’s why we haven’t had to worry about lifetimes until now.

Basically, the rules are as follows:

  • Lifetimes coming into a function are called input lifetimes.
  • Lifetimes returned from a function are called output lifetimes.
  • If there is one input lifetime, it is assigned to all output lifetimes. (Example: fn foo<'a>(bar: &'a str) -> &'a str)
  • If there are many input lifetimes, but one of them is a reference to self, the lifetime of self is assigned to all elided output lifetimes.

I’m just going to link you to the pertinent Rust Book section on this topic, as this article is getting too long.

It has examples and stuff! Read it now!

Yay, we’re done

So that’s what a lifetime is. Hopefully you’ve understood all my incoherent ramblings! If you’d like more information about lifetimes, have a look at the Rust Book (easy), Rustonomicon (slightly more advanced), and head on over to /r/rust if you want to ask us something about lifetimes. There are many nice people there willing to help!


Thanks for reading! Next time in the series, I’ll explain some more difficult lifetime shenanigans, methods for solving them, and I’ll share some problems I’ve personally encountered with lifetimes and how to fix them.