Lifetimes
Rust Ownership
- Every piece of memory in Rust program has exactly one owner at the time
- Ownership changes ("moves")
fn takes_ownership(data: Data)
fn producer() -> Data
let people = [paul, john, emma];
Producing owned data
fn producer() -> String {
String::new()
}
Producing references?
fn producer() -> &str {
// ???
}
&str
"looks" at some string data. Where can this data come from?
Local Data
Does this work?
fn producer() -> &str {
let s = String::new();
&s
}
Local Data
No, we can't return a reference to local data...
error[E0515]: cannot return reference to local variable `s`
--> src/lib.rs:3:5
|
3 | &s
| ^^ returns a reference to data owned by the current function
Local Data
You will also see:
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:18
|
1 | fn producer() -> &str {
| ^ expected named lifetime parameter
|
Static Data
#![allow(unused)] fn main() { fn producer() -> &'static str { "hello" } }
- bytes
h e l l o
are "baked" into your program - part of static memory (not heap or stack)
- a slice pointing to these bytes will always be valid
- safe to return from
producer
function
Note:
You didn't need to specify 'static
for the static variable - there's literally no other lifetime that can work here.
How big is a &'static str
? Do you think the length lives with the string data, or inside the str-reference itself?
(It lives with the reference - so you can take sub-slices)
Static Data
It doesn't have to be a string literal - any reference to a static is OK.
#![allow(unused)] fn main() { static HELLO: [u8; 5] = [0x68, 0x65, 0x6c, 0x6c, 0x6f]; fn producer() -> &'static str { std::str::from_utf8(&HELLO).unwrap() } }
'static
annotation
- Rust never assumes
'static
for function returns or fields in types &'static T
means this reference toT
will never become invalidT: 'static
means that "if typeT
has any references inside they should be'static
"T
may have no references inside at all!
- string literals are always
&'static str
fn takes_and_returns(s: &str) -> &str {
}
Where can the returned &str
come from?
- can't be local data
- is not marked as
'static
- Conclusion: must come from
s
!
Multiple sources
fn takes_many_and_returns(s1: &str, s2: &str) -> &str {
}
Where can the returned &str
come from?
- is not marked as
'static
- should it be
s1
ors2
? - Ambiguous. Should ask programmer for help!
Tag system
fn takes_many_and_returns<'a>(s1: &str, s2: &'a str) -> &'a str {
}
"Returned &str
comes from s2
"
'a
- "Lifetime annotation"
- often called "lifetime" for short, but that's a very bad term
- every reference has a lifetime
- annotation doesn't name a lifetime of a reference, but used to tie lifetimes of several references together
- builds "can't outlive" and "should stay valid for as long as" relations
- arbitrary names:
'a
,'b
,'c
,'whatever
Lifetime annotations in action
fn first_three_of_each(s1: &str, s2: &str) -> (&str, &str) {
(&s1[0..3], &s1[0..3])
}
fn main() {
let amsterdam = format!("AMS Amsterdam");
let (amsterdam_code, denver_code) = {
let denver = format!("DEN Denver");
first_three_of_each(&amsterdam, &denver)
};
println!("{} -> {}", amsterdam_code, denver_code);
}
Annotate!
fn first_three_of_each<'a, 'b>(s1: &'a str, s2: &'b str) -> (&'a str, &'b str) {
(&s1[0..3], &s1[0..3])
}
Annotations are used to validate function body
"The source you used in code doesn't match the tags"
error: lifetime may not live long enough
--> src/lib.rs:2:5
|
1 | fn first_three_of_each<'a, 'b>(s1: &'a str, s2: &'b str) -> (&'a str, &'b str) {
| -- -- lifetime `'b` defined here
| |
| lifetime `'a` defined here
2 | (&s1[0..3], &s1[0..3])
| ^^^^^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a`
|
= help: consider adding the following bound: `'a: 'b`
Annotations are used to validate reference lifetimes at a call site
"Produced reference can't outlive the source"
error[E0597]: `amsterdam` does not live long enough
--> src/main.rs:10:29
|
6 | let amsterdam = format!("AMS Amsterdam");
| --------- binding `amsterdam` declared here
...
10 | first_three_of_each(&amsterdam, &denver)
| --------------------^^^^^^^^^^----------
| | |
| | borrowed value does not live long enough
| argument requires that `amsterdam` is borrowed for `'static`
...
14 | }
| - `amsterdam` dropped here while still borrowed
Lifetime annotations help the compiler help you!
- You give Rust hints
- Rust checks memory access for correctness
fn first_three_of_each<'a, 'b>(s1: &'a str, s2: &'b str) -> (&'a str, &'b str) { (&s1[0..3], &s2[0..3]) } fn main() { let amsterdam = format!("AMS Amsterdam"); let denver = format!("DEN Denver"); let (amsterdam_code, denver_code) = { first_three_of_each(&amsterdam, &denver) }; println!("{} -> {}", amsterdam_code, denver_code); }
What if multiple parameters can be sources?
fn pick_one(s1: &'? str, s2: &'? str) -> &'? str {
if coin_flip() {
s1
} else {
s2
}
}
What if multiple parameters can be sources?
fn pick_one<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if coin_flip() {
s1
} else {
s2
}
}
- returned reference can't outlive either
s1
ors2
- potentially more restrictive
Note:
This function body does not force the two inputs to live for the same amount of time. Variables live for as long as they live and we can't change that here. This just says "I'm going to use the same label for the lifetimes these two references have, so pick whichever is the shorter".
Example
fn coin_flip() -> bool { false } fn pick_one<'a>(s1: &'a str, s2: &'a str) -> &'a str { if coin_flip() { s1 } else { s2 } } fn main() { let a = String::from("a"); let b = "b"; let result = pick_one(&a, b); // drop(a); println!("{}", result); }
Lifetime annotations for types
struct Configuration {
database_url: &str,
}
Where does the string data come from?
Generic lifetime parameter
struct Configuration<'a> {
database_url: &'a str,
}
- An instance of
Configuration
can't outlive a string
that it refers to viadatabase_url
. - The string can't be dropped
while an instance ofConfiguration
still refers to it.
Lifetimes and Generics
- Lifetime annotations act like generics from type system PoV.
- Can be used to to add bounds to types:
where T: Debug + 'a
- Type
T
has to be printable with:?
. - If
T
has references inside, they have to stay valid for as long as'a
tag requires.
- Type
- Can be used to match lifetime generics in
struct
orenum
with the annotations used in function signatures and in turn with exact lifetimes of references.
Complex example
fn select_peer<'a>(peers: &[&'a str]) -> Option<Cow<'a, str>> {
for p in peers {
if is_up(p) {
return Some(Cow::Borrowed(p))
}
}
None
}
fn main() {}
Compiler concludes:
Returned value will not be allowed to outlive any reference in peers
list
let selected = select_peer(&peers);
Lifetime annotations in practice
- Like generics, annotations make function signatures verbose and difficult to read
- they often can be glossed over when reading code
T: 'static
means "Owned data or static references", owned data can be very short-lived- Using owned data in your types helps avoid borrow checker difficulties