2.15 Lifetimes

Lifetimes refer to that of a variable entering scope, until that of it exiting scope and ultimately being destroyed:

fn main()
{
	// ‘a’ starts living
	let a = 5;

	{
		// ‘b’ starts living
		let b = 3;

		// ‘b’ goes out of scope
		// its lifetime is over
	}

	// ‘a’ goes out of scope
	// its lifetime is over
}

Here a enters scope at the beginning of its code-block. Another scope is created by another code-block, whereas b goes into scope. At the end of that code-block, b leaves scope, its lifetime is over. Finally, by the end of main’s scope, a goes out of scope, rendering it dead. Its lifetime is over.

Time of life

Imagine we have a situation like this:

fn main()
{
	let a;

	{
		let b = 3;

		a = &b;

		// ‘b’ leaves scope, thus
		// rendering the resource its bound to
		// useless
	}

	// We attempt printing ‘a’ out, 
	// but it is bound to the resource
	// of ‘b’, which no longer exists
	// For that reason, this will not work
	println!("{}", a);
}

We set a to a reference referring to the resource bound to b, but since this goes out of scope prior to a printing out, there no longer is a value to print. For that fact, this will present us with an error saying:

error: `b` does not live long enough
 --> src\main.rs:8:8
  |
8 |             a = &b;
  |                  ^
  |

The Rust compiler already knows there is something silly going on; it knows we cannot reference a resource whose lifetime is about to end. In order for us to reference something, we need to make sure that the resource we reference lives longer than the binding referencing it.

Functions

It is highly unlikely that one would be silly enough to ever use the previous example inside an actual application. The real trouble comes upon using functions, as things get slightly more complicated at times:

n main()
{
	let a = String::from("Hello, world!");
	
	// ‘b’ is set to a reference of ‘a’
	let b = string(&a);

	// Since ‘b’ only borrows the resource of ‘a’, 
	// ‘a’ may still correctly print out
	println!("{}", a);
}

fn string(c: &String) -> &String
{
	c
}

This does indeed compile very nicely, as well as print out Hello, world! into the console. Although this may look straight forward, there is a whole other aspect going on beneath the surface of Rust. Had the compiler not been as clever as it is, this would have generated a rather troubling error.

Elision

The Rust compiler, which is sometimes called the ‘borrow checker’, makes use of something referred to as ‘lifetime elision’. Had there been no such thing, we would have had to manually input all the different lifetimes in order for Rust to know how long different bindings and their resources live. This is what Rust does for us on the string function:

fn string<'a>(c: &'a String) -> &'a String
{
	c
}

Inside the <> after string, we tell Rust that this function has one lifetime called a. This lifetime is the same for the reference given the function, as well as the reference returned by the function. This makes sense, considering it is the reference given the function we return back from it.

Rust needs this information so that it can validate the lifetimes of various references given and taken from the function.

Rules

As for functions, this has rules as well:

  • References are required to have annotated lifetimes, unless the pattern is common enough for Rust’s ‘lifetime elision’ to figure out what lifetime goes where on its own.
  • Returned references must have the same lifetimes as another variable being inputted into the function.

To clarify, all references given or returned from funtions are required to have lifetimes, even though ‘lifetime elision’ sometimes does the job for you.

Mutability

Even for mutable references, there needs be lifetimes:

fn main()
{
	let mut a = 32;

	dec(&mut a);

	println!("{}", a);
}

fn dec<'a>(b: &'a mut i32)
{
	*b -= 1;
}

Although this, too, like all the previous examples, has lifetimes taken care of by ‘lifetime elision’, there are times when you need to do it manually. As long as you understand the underlying principles of all Rust’s principles, it should be no problem to you.

Exercises

  • Make a rather obvious function taking a reference and returning the same reference.
  • Contemplate this section a hundred times over.
  • Get mad.

Conclusion

Lifetimes are what people usually find the most difficult about in Rust, but once they are grasped and the ‘borrow checker’ has been fought many times over, Rust will turn out to be one amazing language!

The reason there are lifetimes, is for Rust to be able to be absolutely certain that it remains one of the world’s safest programming languages. Without lifetimes, Rust would prove highly errorprone, sometimes resulting in horrible outcomes.