3
votes

Lets say I have a function with the following signature in Rust:

fn f<'a>(x: &'a i32) -> &'a i32;

Lets say I do the following then:

let x = 0;
let y = f(&x);

In that case, the Rust borrow checker considers y to borrow x. Why? What is the reason on a deeper level than "because you used the same lifetime parameter in the parameter type and the return type".

2

2 Answers

5
votes

The function signature

fn f<'a>(x: &'a i32) -> &'a i32;

means that the value returned by f is a reference to what x parameter refers to, hence it can't outlive it. For example, this won't work:

// Compile error
let y = {
    let x = 0;
    f(&x)

    // x is dropped here
};

// Here y still "exists", but x doesn't (y outlives x)

To your specific question:

Lets say I do the following then:

let x = 0;
let y = f(&x);

In that case, the Rust borrow checker considers y to borrow x. Why?

The answer is because the function signature of f tells it so. To give you an example, suppose that we change the signature to this:

fn f<'a, 'b>(x: &'a i32, z: &'b i32) -> &'a i32;

Then we call f like this:

let x = 0;
let z = 1;
let y = f(&x, &z);

In the code above, y borrows x, but not z. It's because the return value of f has 'a lifetime that is the same as x's lifetime.

4
votes

Using the syntax from the Rustonomicon, we can elaborate the second snippet

let x: i32 = 0;
'a: { // definition of a lifetime (not real syntax)
    let y: &'a i32 = f::<'a>(&'a x) // &'a x is also not real, though interestingly rustc recognizes it
}

The lifetime 'a is introduced by the compiler because you wrote a & and it needs to know how long that borrow will last (note that you cannot manually specify lifetimes for borrows). The type of the function also means that y has 'a in its type. The compiler needs to figure out where 'a starts and ends. The rules are that 'a must start right before you perform the borrow, that x cannot be moved during 'a (since it's borrowed), and that y cannot be used after 'a (because its type says so). If the compiler can pick start and end points for 'a so that the rules hold, then it compiles. If it can't, there's an error. There is no direct relationship between x and y; it goes through the lifetime 'a that they both interact with. E.g. you can't move x and then read *y because 'a must end before you move x and after you read *y, but such a time does not exist. TL;DR think about borrow checking as lifetime inference.