Search code examples
rustlifetime

How to understand the lifetime of function parameters and return values in Rust?


I'm a newbie in Rust and I'm still struggling with lifetime in Rust. The Rust Programming Language book defines lifetime as

the scope for which that reference is valid

It's easy to understand when the context is a single function. For example, in the below code, the lifetime of s is the blue box, the lifetime of x is the green box etc.

enter image description here

When it comes to functions, I don't quite understand what exactly does the lifetime mean for function parameters and return values. Let's say we have this function:

fn parse_record<'i>(input: &'i [u8]) -> Record<'i> { ... }

The signature states that the input parameter and the return value Record must have the same lifetime 'i. Does that mean when we are calling the function, the value we passed into the function and the returned value must have the same lifetime? For example, I may invoke the function in main function like this:

fn main() {
    let mut v: Vec<u8> = [1_u8, 2_u8, 3_u8].to_vec();
    let result = parse_record(&v);
    // use v and result ...
}

Does the lifetime in the function signature state that v and result in main must have the same lifetime?


Solution

  • I would like to clarify here, citing Rust by Example

    A lifetime is a construct the compiler (or more specifically, its borrow checker) uses to ensure all borrows are valid. Specifically, a variable's lifetime begins when it is created and ends when it is destroyed. While lifetimes and scopes are often referred to together, they are not the same.

    Take, for example, the case where we borrow a variable via &. The borrow has a lifetime that is determined by where it is declared. As a result, the borrow is valid as long as it ends before the lender is destroyed. However, the scope of the borrow is determined by where the reference is used.

    It seems that Rust book created a lot of confusion, but scope and lifetime are indeed different things, when we talk about simple bindings which own data (not borrow it) lifetime and scope match together.

    If we have simple code like this. Lifetime of a and b will match with scope they are defined.

    fn main() {
        let a = 1;
        let b = 2;
    }
    

    In this example, lender (a) goes out of scope sooner than borrow.

    fn main() {
    
        let b;
        {
            let a = 1;
            b = &a;
        }
        let c = *b;
    }
    

    It forces compiler to emit error.

    
    error[E0597]: `a` does not live long enough
     --> src/main.rs:5:9
      |
    5 |         b = &a;
      |         ^^^^^^ borrowed value does not live long enough
    6 |     }
      |     - `a` dropped here while still borrowed
    7 |     let c = *b;
      |             -- borrow later used here
    

    So here b has a lifetime longer than the lifetime of a, because it has a greater scope. Remember that scope of borrow is determined by where the reference is used.

    But this code compiles just fine, because b scope doesn't end after a is dropped.

    fn main() {
        let b;
        {
            let a = 1;
            b = &a;
            let c = *b;
        }
    }
    

    Another thing to clarify is that lifetime syntax for reference &'lifetime means. It means that reference should live as long as 'lifetime lifetime. It shouldn't be exactly that lifetime.

    Suppose Record element is defined like this.

    struct Record<'a> {
       some_member: &'a Type
    }
    

    This signature just means that some member of record should live as long as reference passed to input or vice versa.

    fn parse_record<'i>(input: &'i [u8]) -> Record<'i> { ... }
    

    If I translate it to plain English. Lender of the reference passed to the function shouldn't go out of scope (dropped) as long as a field inside Record didn't go out of scope.

    Or return value of function should live as long as input argument of function.

    If we don't have return value which is constrained by the input lifetime, translation changes.

    fn test<'a>(a: &'a i32, b: &'a i32)
    

    This means that lenders of a and b should be in scope together until function execution ends.

    In many simple cases lifetimes are elided by compiler and you shouldn't worry about them. Actually in your example too, lifetimes can be elided.

    fn parse_record(input: &[u8]) -> Record { ...  }
    

    I recommend you to read Rust by Example chapters on lifetimes to more practically understand them.