Lifetimes

From Rust Community Wiki
Jump to navigation Jump to search

In Rust, lifetimes are the mechanism by which the Rust compiler can reason about the logic of the scope of variables in your program. Lifetimes are denoted by a single quote followed by an identifier, which is typically one letter - for example 'a. Lifetimes behave similarly to types, but come with a few important differences:

  • Lifetimes are always anonymous. This means that they can be referred to only from generic lifetime parameters - there's no such thing as a concrete lifetime.
  • Lifetimes can often be elided - most of the time Foo<'a> can be replaced with Foo<'_> or even just Foo. Type parameters on the other hand must always be specified.
  • Lifetimes do not have any direct correspondance with a the resulting binary of the program like types do - they exist only in the compiler. As a result, lifetime parameters do not cause functions to be monomorphized, and they can be used in trait objects.

The static lifetime[edit | edit source]

The is one special lifetime in Rust that is different from all the rest - the 'static lifetime. While other lifetimes represent short-lived values on the stack, the 'static lifetime represents a value that exists for the entirety of the duration of the program.

All lifetimed static and const varibles use the static lifetime - in fact, you can omit 'static since it is inferred anyway.

fn assert_static<T>(_reference: &'static T) { }

const MY_CONST: i32 = 42;
assert_static(&MY_CONST);

static MY_STATIC: i32 = 42;
assert_static(&MY_STATIC);

'static as trait bound:

fn assert_static<T: 'static>(_: T) { }

const MY_CONST: i32 = 42;
assert_static(&MY_CONST);
assert_static(MY_CONST);

static MY_STATIC: i32 = 42;
assert_static(&MY_STATIC);
assert_static(MY_STATIC);

let normal_var: i32 = 42;
assert_static(normal_var); // NOT reference

Example[edit | edit source]

// some_function can be called with any lifetime 'a.
//
// We use the same lifetime for the parameter and the return value to show that
// they exist for the same time (as in this case they point to the same value).
fn some_function<'a>(x: &'a i32) -> &'a i32 { x }

let x = 5; // We will now denote x's lifetime as 'x

let y = some_function(&x); // some_function is called with the lifetime 'x
// y now also has the lifetime 'x

Lifetime elision[edit | edit source]

Writing out lifetimes is a pain. In the example above, we had to manually specify the lifetime of the references - this just adds boilerplate and is annoying to have to type out. Luckily, in a few specific cases the compiler can help us. If there is one elided lifetime in the parameters of a function or there is a self parameter with an elided lifetime (such as &self or &mut self, then all elided lifetimes in the return type of that function will be that lifetime.

There are two levels of lifetime elision: anonymous lifetimes and implicit lifetimes. Anonymous lifetimes look like this: '_ and can be used in the place of regular lifetimes: &'_ i32. In function parameters (but not on async functions), you can omit the lifetime entirely, and then it is implicit: &i32.

The automatic lifetime elision resolver covers 99% of cases. If you are writing something so complex that it doesn't get accepted, it is probably better to be explicit about the lifetimes anyway.

Higher Ranked Trait Bounds[edit | edit source]

In very specific scenarios, a concrete lifetime will not be known to a function until runtime. In which case, a function must work for every possible lifetime without regard to whether it is short or long. This might be caused by running a closure in multiple places, or writing a function that takes a reference to an object that hasn't been created yet. Consider the example code:

#[derive(Debug, Clone)]
struct MyStruct(u32);

fn with_my_struct<T, F>(func: F) -> T 
where
    F: FnOnce(&MyStruct) -> T
{
    let struct_instance = MyStruct(32);

    func(&struct_instance)

    // struct_instance is dropped here
}

fn main() {
    // GOOD: Does not return a reference to struct_instance
    with_my_struct(|struct_ref| {
        println!("struct_ref contains {}", struct_ref.0);
        struct_ref.clone()
    });
    
    // BAD: invalid_reference references a struct that has been dropped.
    // let invalid_reference = with_my_struct(|struct_ref| {
    //     struct_ref
    // });
}

struct_instance is created at the beginning of with_my_struct and dropped at the end. That means that func is not allowed to return a reference to struct_instance, otherwise there would be a reference to a dropped struct. Another way of thinking about this would be to say that func has to be valid for every lifetime, even for lifetimes that are too short to be in scope yet. Luckily, Rust catches that the second example is not allowed, and gives a lifetime error. However, the waters can be muddied by the addition of other references. In the following example, lifetime annotations are required:

#[derive(Debug, Clone)]
struct MyStruct(u32);

fn with_my_struct<'l, T, F>(
    ong_lived_instance: &'l MyStruct,
    func: F,
) -> T
where
    F: for<'s> FnOnce(&'s MyStruct, &'l MyStruct) -> T
{
    let short_lived_instance = MyStruct(32);

    func(&short_lived_instance, long_lived_instance)
    
    // short_lived_instance is dropped here
}

fn main() {
    let long_lived_instance = MyStruct(48);

    // GOOD!
    let valid_reference = 
        with_my_struct(&long_lived_instance, |_, long_lived| long_lived);

    // BAD: invalid_reference references a struct that has been dropped.
    // let invalid_reference =
    //     with_my_struct(&long_lived_instance, |short_lived, _| short_lived);

    // long_lived_instance gets dropped here
}

Rust needs help to know that the arguments to func have different lifetimes: a short lifetime called 's and a long lifetime called 'l. It's okay to return long_lived because that references long_lived_instance, and long_lived_instance is dropped at the end of main. However it's not okay to return short_lived because that references short_lived_instance and short_lived_instance is dropped at the end of with_my_struct.

The for<'s> in the code above tells rust that func is valid for any lifetime wherever 's is used. The 'l tells Rust that func is only valid for an existing lifetime 'l that is still valid after func returns.