Fixing a Simple Lifetime Error in Rust

Lifetimes are perhaps the hardest thing to understand when first approaching Rust. Today I’m going to create a lifetime error and demonstrate two strategies for fixing it. If you’re very new to rust, you may wish also to read the (beautifully titled) post Rust Lifetimes for the Unitialised first, as well as Strategies for Returning References in Rust.

To get started, we’re going to consider two Ruby implementations that are equivalent:

At the end of both Ruby snippets, my_collection is full of values like “value number 1”. Not a terribly interesting program, but it will help us illustrate lifetimes in Rust. Below is an attempt to write the same program in Rust, but it won’t compile. Today we’re going to talk about why it won’t compile, and discuss two changes that would make it compile. Here’s the Rust program:

If we try to build the above program, we get the following issue:

   Compiling playground v0.0.1 (file:///playground)
error[E0597]: `value` does not live long enough
  --> src/
19 |         my_collection.insert(&value);
   |                               ^^^^^ borrowed value does not live long enough
20 |     });
   |     - `value` dropped here while still borrowed
21 | }
   | - borrowed value needs to live until here

error: aborting due to previous error

error: Could not compile `playground`.

To learn more, run the command again with --verbose.

This seems like a very simple program, and it’s at first discouraging to see errors like this when learning Rust. But let’s dive into this error.

Rust tells us that the “borrowed value does not live long enough.” What this means is that my_collection, which is owned by the main() function, cannot have a reference to value which is owned by the anonymous function passed to for_each() on line 17, because value goes out of scope on line 20, and my_collection has to be valid to the end of main().

In Rust terms, “lifetime a” begins at line 16, when my_collection is created. my_collection stores a collection of borrowed strings of lifetime a. That’s what Vec<&'a str> means in line 2: “A vector of borrowed strings of lifetime a”. The value created by &value in line 19 has a different lifetime: it lives from line 19 to line 20. Call it “lifetime b”. Because lifetime b ends before lifetime a, we can’t use a value with lifetime b where the compiler expects a value of lifetime a. We can almost think of this as type checking: you can’t put an &'b str in a vector of &'a str unless b outlives a.

But if the strings don’t live long enough in Rust, how do the Ruby programs work?

The reason the Ruby program in example 1 above is able to work is that Ruby has a runtime garbage collector, which will notice that value needs to live as long as my_collection has a reference to it. Rather than having lifetimes, Ruby has a lifeguard that keeps track of how long different values need to be around and keeps them alive that long. This language feature prevents programmers from having to worry about lifetimes, but it also has a significant runtime cost. The same thing is true in most garbage-collected programming languages.

To avoid this cost, Rust doesn’t have a garbage collector. At runtime, &value is little more than a pointer, and nothing is telling it to live as long as my_collection, so it won’t. Fortunately, the Rust compiler is smart enough to detect that the reference doesn’t live long enough and complain at compile time, rather than letting you dereference a no-longer-valid pointer at runtime.

Now that we have a basic notion of Rust lifetimes, we can understand two basic ways of solving the borrow checker error that we had above.

The first solution to this problem is to put the strings somewhere that does live long enough:

What this does is give your borrowed elements a home. Now, when you make a new string, you give ownership of it to the vector values. Now objects who have references (who “borrow”) these values may do so as long as they go out of scope after the vector does.

There are two small gotchas with this approach.

The first is that the vector must own the strings. That is, it must have the type Vec<String> not Vec<&str>. (If you try to add values to a Vec<&str>, you’ll encounter the same problem we were originally trying to solve: vector will be asking to borrow something that goes out of scope before it does.)

The second gotcha I only caught because of an amazing compiler error message (I mean it – the rust compiler is super helpful):

note: values in a scope are dropped in the opposite order they are created

What that means is that, in this example, you have to declare values above my_collection; you’ll see the above error message if you switch lines 16 and 17 in the gist above.

That covers the first solution to the lifetime issue. In short, you have to put the values somewhere that they do live long enough.

The second solution is for the collections to take ownership of the values. Note that you can employ this solution only if you are able to change the type signature of the method that’s complaining about the lifetime.

The solution here is not trying to use borrows at all. Because my_collection is a collection of strings, and not of borrowed strings, they will live as long as it does.

The lesson here is that we can’t arbitrarily return values from a narrower scope. In garbage collected languages, it’s perfectly normal to build up a value in a local variable and then return it to the calling scope. In Rust, we can do this to, but we have to return ownership of the local variable. If we try to return a reference (in Rust terminology, a “borrow”) of a variable from a smaller scope into a larger scope, Rust will remind us that we’re breaking the rules about lifetimes.

Till next time, happy learning!


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s