The longest string is long string is long
()
Performant Software Systems with Rust — Lecture 9
Lifetimes — a First Cut
Every reference has a lifetime
Just like type inference, lifetimes are inferred by the compiler in most cases
But also like type annotation, we must annotate lifetimes when inference is not possible
Consider This Example
The Borrow Checker
x
’s lifetime, 'b
, is not as long as r
’s lifetime, 'a
x does not live long enough!
Learn more: The Borrow Checker
Lifetime Annotations in Functions
Learn more: Generic Lifetimes in Functions
Implementing longest()
Learn more: Generic Lifetimes in Functions
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
Learn more: Generic Lifetimes in Functions
Lifetime Annotations
We need to define the relationship between the references using lifetime annotations, so the borrow checker can perform its analysis
Lifetime annotations don’t change how long any of the references live — they are just hints to the borrow checker
Learn more: Lifetime Annotation Syntax
Revisiting longest()
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// the returned reference will live as long as 'a
// or, the returned reference will be valid as long as both the
// parameters are valid
// or, the returned reference cannot outlive either x or y
// or, the lifetime of the returned reference is the same as the
// smaller of the lifetimes of the two references passed in
...
}
Learn more: Lifetime Annotations in Function Signatures
The Borrow Checker: Working with Annotated Lifetimes
The longest string is long string is long
()
Learn more: Lifetime Annotations in Function Signatures
The Borrow Checker: Working with Annotated Lifetimes
Learn more: Lifetime Annotations in Function Signatures
error[E0597]: `s2` does not live long enough
--> src/main.rs:14:39
|
13 | let s2 = String::from("xyz");
| -- binding `s2` declared here
14 | result = longest(s1.as_str(), s2.as_str());
| ^^ borrowed value does not live long enough
15 | }
| - `s2` dropped here while still borrowed
16 | println!("The longest string is {result}");
| -------- borrow later used here
Learn more: Lifetime Annotations in Function Signatures
Lifetime Annotations in Structs
If a struct holds references, we need a lifetime annotation for each reference
struct ImportantExcerpt<'a> {
// an instance of `ImportantExcerpt` can’t outlive the reference
// it holds in `part`
part: &'a str,
}
fn main() {
let novel = String::from("Rust vs. C++");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
println!("{}", i.part);
}
Learn more: Lifetime Annotations in Struct Definitions
Lifetime Elision
Each input parameter gets its own lifetime
If there is exactly one input lifetime parameter, its lifetime is assigned to all output lifetime parameters
If there are multiple input lifetime parameters, but one of them is &self
or &mut self
because this is a method, the lifetime of self
is assigned to all output lifetime parameters
Learn more: Lifetime Elision
Lifetime Elision: Example
Lifetime Annotations in Method Definitions
The 'static
Lifetime
A special lifetime that tells the compiler that the reference can live for the entire duration of the program
All string literals have the 'static
lifetime, as they are in static memory (or the data segment)
Watch out on following the compiler’s suggestions — do not use 'static
lifetimes if you don’t know what you are doing
Learn more: The Static Lifetime
Anonymous Lifetimes
The compiler should try to infer the lifetime annotation by itself
It is typically for simplifying the grammar when
impl
blocksLearn more: Special Lifetimes
Let’s revisit our first example:
There’s just one problem, though.
Let’s consider the following revised code:
Now the code compiles successfully. But why?
Lifetime vs. Scope: Revised Code
Lifetime vs. Scope: Original Example
Let’s Consider Another Example
Now the code compiles successfully. But why?
Let’s Look At Lifetimes Again
Let’s Make Another Revision
The revised code also compiles successfully. But why?
Let’s Look At a Third Example
use rand::Rng;
fn main() {
let mut rng = rand::rng();
let mut x = String::from("Hello");
let random_float: f64 = rng.random();
let r = &x; // -----+- 'b, immutable borrow on x
// |
if random_float > 0.5 {
// 'b is not alive
x.push_str(" World!"); // 'b is not alive, mutable borrow on x
} else {
// |
println!("{r}"); // -----+- 'b
}
}
This example compiles and runs successfully!
But What If We Add One Line of Code?
use rand::Rng;
fn main() {
let mut rng = rand::rng();
let mut x = String::from("Hello");
let random_float: f64 = rng.random();
let r = &x; // -----+- 'b, immutable borrow on x
// |
if random_float > 0.5 { // 'b is alive
x.push_str(" World!"); // 'b is alive, mutable borrow on x
println!("{r}"); // immutable borrow!
} else { // |
println!("{r}"); // -----+- 'b
}
}
error[E0502]: cannot borrow `x` as mutable because it is also
borrowed as immutable
--> src/main.rs:11:9
|
7 | let r = &x;
| -- immutable borrow occurs here
...
11 | x.push_str(" World!");
| ^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
12 | println!("{r}");
| --- immutable borrow later used here
The data flow model
— Chapter 1, Foundations, in Rust for Rustaceans, Jon Gjengset
References
&
&mut
Learn more: References
Aliasing
Variables and pointers alias if they refer to overlapping regions of memory
Can the Rust compiler optimize the previous code to the following?
Learn more: Aliasing
Alias Analysis Helps
We used the fact that &mut u32
can’t be aliased to prove that writes to *output
can’t possibly affect *input
This lets us cache *input
in a register, eliminating a read
Alias analysis lets the compiler perform useful optimizations!
Learn more: Aliasing
But should we be concerned with aliasing in the following modified code?
No. input
doesn’t alias temp
, because the value of a local variable can’t be aliased by things that existed before it was declared.
Learn more: Aliasing
Now Let’s Revisit Lifetimes
And the compiler sees
'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 Rust Programming Language, Chapter 10.3
The Rustonomicon, Chapter 3.1 - 3.3