Why do we need lifetimes in functions?

Hey. I am a beginner in Rust.

I don't understand, why we need lifetimes in functions if we still borrow arguments?
Regardless of which of the arguments is returned, or no any of them, we can still use any argument that we passed to the functions.

fn return_diff_lifetime_1 <'a,'b> (x: &'a str, y: &'b str) -> &'a str { x }
fn return_diff_lifetime_2 <'a,'b> (x: &'a str, y: &'b str) -> &'b str { y }
fn return_same_lifetime_1 <'a>    (x: &'a str, y: &'a str) -> &'a str { x }
fn return_same_lifetime_2 <'a>    (x: &'a str, y: &'a str) -> &str    { x }
fn return_not_an_argument <'a>    (x: &'a str, y: &'a str) -> &str    { "not an arg" }

let (x, y) = ("test1", "test2");

return_diff_lifetime_1 (x, y);
return_diff_lifetime_2 (x, y);
return_same_lifetime_1 (x, y);
return_same_lifetime_2 (x, y);
return_not_an_argument (x, y);

println!("{:?}", x); // "test1"
println!("{:?}", y); // "test2"

Also, the removal of lifetimes in the example from the book will not affect anything

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

It looks like we don't need lifetimes in function signature because it doesn't affect anything

  1. None of your function call's return value persisted more than 1 line. You need to bind them to a variable and use them later.

  2. Ref(&) can coexist anyway, you need also change you argument to &mut i32 to see a difference.

Lifetime annotation is a relation between all the arguments' lifetimes in a function.
So even if you don't annotate the lifetime in some simple cases, Rust will insert lifetimes for you, which is called lifetime elision.
We need the lifetime annotation because Rust can deduce basic lifetime information from the function's signature, and easily check your code about lifetime.
It's possible theoretically to look into the function body to track the lifetime of returned value, but this will become complicated and cause longer time in compilation.

For fn return_same_lifetime_2 <'a> (x: &'a str, y: &'a str) -> &str, which has a single lifetime parameter, Rust assumes the returned reference must live at least as long as 'a, and checks that.
For fn return_diff_lifetime_2 <'a,'b> (x: &'a str, y: &'b str) -> &str, which has two lifetime parameters, Rust won't choose one from the signature, so you need to specify the relation.
For fn return_not_an_argument <'a> (x: &'a str, y: &'a str) -> &str { "not an arg" }, the type of "not an arg" is &'static str which can shorten to any lifetime.

Here are a couple examples to illustrate some of the differences

    let a = "a".to_string();
    let b = "b".to_string();
    let x = return_diff_lifetime_1(&a, &b);
    drop(a);
    // errors because the signature says the return value is related to the first argument,
    // thus accessing x after a has been dropped is not allowed
    // using return_diff_lifetime_2 would allow this to compile
    println!("{x}");
    
    let e = "e";
    let f = "f".to_string();
    // errors because even though the function actually returns e which is a &'static str,
    // the signature says the return value may borrow from both/either argument
    // using return_diff_lifetime_1 would allow this to compile
    let s: &'static str = return_same_lifetime_1(e, &f);
1 Like

Here's some examples which don't compile.

fn return_diff_lifetime_3 <'a,'b> (x: &'a str, y: &'b str) -> &'a str { y }
fn return_diff_lifetime_4 <'a,'b> (x: &'a str, y: &'b str) -> &'b str { x }
fn return_some_lifetime_1 <'a,'b> (x: &'a str, y: &'b str) -> &   str { x }
fn return_some_lifetime_2         (x: &   str, y: &   str) -> &   str { x }

For the first two, you can't say you're going to return one lifetime and then return an incompatible lifetime instead. Callers can count on the return borrow lasting as long as the matching passed-in reference. (Consider if they passed in a &'static str for one, but not both, arguments.)

For the second, you can't return a borrow where it's ambiguous what lifetime it has (taking into consideration the elision rules).


Here:

fn return_not_an_argument<'a>(x: &'a str, y: &'a str) -> &str { "not an arg" }

You're returning a &'static str, which can coerce to a &'any_lifetime str, so it's compatible with 'a. You could do something similar for all the other compiling examples too.


Here:

fn return_same_lifetime_2<'a>(x: &'a str, y: &'a str) -> &str { x }

This should be an error as it doesn't conform to any elision rule, but apparently this is a new bug in Rust 1.64. [1] It's considered ambiguous like return_some_lifetime_* in Rust 1.63 and before. (Arguably it could be allowed as a new elision rule where the elided lifetime is also 'a.)

It looks like we don't need lifetimes in function signature because it doesn't affect anything

As you progress in your learning, you'll find that it affects a lot (there are questions about solving lifetime constraints on this forum pretty much every day).


  1. Just filed due to this thread :sweat_smile: ↩ī¸Ž

5 Likes

I'm surprised about that. I can't remeber a lifetime elision rule when seeing fn return_same_lifetime_2<'a>(x: &'a str, y: &'a str) -> &str, and I thought it was newly added in Rust...

I didn't see anything in the release notes. #98835 had a lot of lifetime-related fallout, but there are no closures involved here. I haven't really dug at all yet, though.