Lifetimes
Rust enforces these rules through lifetimes. Lifetimes are namedregions of code that a reference must be valid for. Those regionsmay be fairly complex, as they correspond to paths of executionin the program. There may even be holes in these paths of execution,as it’s possible to invalidate a reference as long as it’s reinitializedbefore it’s used again. Types which contain references (or pretend to)may also be tagged with lifetimes so that Rust can prevent them frombeing invalidated as well.
In most of our examples, the lifetimes will coincide with scopes. This isbecause our examples are simple. The more complex cases where they don’tcoincide are described below.
Within a function body, Rust generally doesn’t let you explicitly name thelifetimes involved. This is because it’s generally not really necessaryto talk about lifetimes in a local context; Rust has all the information andcan work out everything as optimally as possible. Many anonymous scopes andtemporaries that you would otherwise have to write are often introduced tomake your code Just Work.
However once you cross the function boundary, you need to start talking aboutlifetimes. Lifetimes are denoted with an apostrophe: 'a
, 'static
. To dipour toes with lifetimes, we’re going to pretend that we’re actually allowedto label scopes with lifetimes, and desugar the examples from the start ofthis chapter.
Originally, our examples made use of aggressive sugar — high fructose cornsyrup even — around scopes and lifetimes, because writing everything outexplicitly is extremely noisy. All Rust code relies on aggressive inferenceand elision of “obvious” things.
One particularly interesting piece of sugar is that each let
statementimplicitly introduces a scope. For the most part, this doesn’t really matter.However it does matter for variables that refer to each other. As a simpleexample, let’s completely desugar this simple piece of Rust code:
let x = 0;
let y = &x;
let z = &y;
The borrow checker always tries to minimize the extent of a lifetime, so it willlikely desugar to the following:
// NOTE: `'a: {` and `&'b x` is not valid syntax!
'a: {
let x: i32 = 0;
'b: {
// lifetime used is 'b because that's good enough.
let y: &'b i32 = &'b x;
'c: {
// ditto on 'c
let z: &'c &'b i32 = &'c y;
}
}
}
Wow. That’s… awful. Let’s all take a moment to thank Rust for making this easier.
Actually passing references to outer scopes will cause Rust to infera larger lifetime:
let x = 0;
let z;
let y = &x;
z = y;
'a: {
let x: i32 = 0;
'b: {
let z: &'b i32;
'c: {
// Must use 'b here because this reference is
// being passed to that scope.
let y: &'b i32 = &'b x;
z = y;
}
}
}
Example: references that outlive referents
Alright, let’s look at some of those examples from before:
fn as_str(data: &u32) -> &str {
let s = format!("{}", data);
&s
}
desugars to:
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s;
}
}
This signature of as_str
takes a reference to a u32 with some lifetime, andpromises that it can produce a reference to a str that can live just as long.Already we can see why this signature might be trouble. That basically impliesthat we’re going to find a str somewhere in the scope the referenceto the u32 originated in, or somewhere even earlier. That’s a bit of a tallorder.
We then proceed to compute the string s
, and return a reference to it. Sincethe contract of our function says the reference must outlive 'a
, that’s thelifetime we infer for the reference. Unfortunately, s
was defined in thescope 'b
, so the only way this is sound is if 'b
contains 'a
— which isclearly false since 'a
must contain the function call itself. We have thereforecreated a reference whose lifetime outlives its referent, which is literallythe first thing we said that references can’t do. The compiler rightfully blowsup in our face.
To make this more clear, we can expand the example:
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s
}
}
fn main() {
'c: {
let x: u32 = 0;
'd: {
// An anonymous scope is introduced because the borrow does not
// need to last for the whole scope x is valid for. The return
// of as_str must find a str somewhere before this function
// call. Obviously not happening.
println!("{}", as_str::<'d>(&'d x));
}
}
}
Shoot!
Of course, the right way to write this function is as follows:
fn to_string(data: &u32) -> String {
format!("{}", data)
}
We must produce an owned value inside the function to return it! The only waywe could have returned an &'a str
would have been if it was in a field of the&'a u32
, which is obviously not the case.
(Actually we could have also just returned a string literal, which as a globalcan be considered to reside at the bottom of the stack; though this limitsour implementation just a bit.)
Example: aliasing a mutable reference
How about the other example:
let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);
'a: {
let mut data: Vec<i32> = vec![1, 2, 3];
'b: {
// 'b is as big as we need this borrow to be
// (just need to get to `println!`)
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
// Temporary scope because we don't need the
// &mut to last any longer.
Vec::push(&'c mut data, 4);
}
println!("{}", x);
}
}
The problem here is a bit more subtle and interesting. We want Rust toreject this program for the following reason: We have a live shared reference x
to a descendant of data
when we try to take a mutable reference to data
to push
. This would create an aliased mutable reference, which wouldviolate the second rule of references.
However this is not at all how Rust reasons that this program is bad. Rustdoesn’t understand that x
is a reference to a subpath of data
. It doesn’tunderstand Vec
at all. What it does see is that x
has to live for 'b
tobe printed. The signature of Index::index
subsequently demands that thereference we take to data
has to survive for 'b
. When we try to call push
,it then sees us try to make an &'c mut data
. Rust knows that 'c
is containedwithin 'b
, and rejects our program because the &'b data
must still be live!
Here we see that the lifetime system is much more coarse than the referencesemantics we’re actually interested in preserving. For the most part, that’stotally ok, because it keeps us from spending all day explaining our programto the compiler. However it does mean that several programs that are totallycorrect with respect to Rust’s true semantics are rejected because lifetimesare too dumb.
The area covered by a lifetime
The lifetime (sometimes called a borrow) is alive from the place it iscreated to its last use. The borrowed thing needs to outlive only borrows thatare alive. This looks simple, but there are few subtleties.
The following snippet compiles, because after printing x
, it is no longerneeded, so it doesn’t matter if it is dangling or aliased (even though thevariable x
technically exists to the very end of the scope).
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x);
// This is OK, x is no longer needed
data.push(4);
However, if the value has a destructor, the destructor is run at the end of thescope. And running the destructor is considered a use ‒ obviously the last one.So, this will not compile.
#[derive(Debug)]
struct X<'a>(&'a i32);
impl Drop for X<'_> {
fn drop(&mut self) {}
}
let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
data.push(4);
// Here, the destructor is run and therefore this'll fail to compile.
Furthermore, there might be multiple possible last uses of the borrow, forexample in each branch of a condition.
# fn some_condition() -> bool { true }
let mut data = vec![1, 2, 3];
let x = &data[0];
if some_condition() {
println!("{}", x); // This is the last use of `x` in this branch
data.push(4); // So we can push here
} else {
// There's no use of `x` in here, so effectively the last use is the
// creation of x at the top of the example.
data.push(5);
}
And a lifetime can have a pause in it. Or you might look at it as two distinctborrows just being tied to the same local variable. This often happens aroundloops (writing a new value of a variable at the end of the loop and using it forthe last time at the top of the next iteration).
let mut data = vec![1, 2, 3];
// This mut allows us to change where the reference points to
let mut x = &data[0];
println!("{}", x); // Last use of this borrow
data.push(4);
x = &data[3]; // We start a new borrow here
println!("{}", x);
Historically, Rust kept the borrow alive until the end of scope, so theseexamples might fail to compile with older compilers. Also, there are still somecorner cases where Rust fails to properly shorten the live part of the borrowand fails to compile even when it looks like it should. These’ll be solved overtime.