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 to T will never become invalid
  • T: 'static means that "if type T 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 or s2?
  • 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 or s2
  • 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 via database_url.
  • The string can't be dropped
    while
    an instance of Configuration 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.
  • Can be used to match lifetime generics in struct or enum 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