Lifetime example from the book does not seem to be showing the real need for lifetime specifiers

In the book there is an example about lifetime

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

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

I understand that the reason Rust does not accept this is because the elision rules don't cover this case, but I don't understand why don't they cover.

In other words, it seem obvious (to my naive eyes) that both variables that were sent in the function will outlive the return value so at least theoretically this could be covered in the elision rules.

I feel, probably incorrectly, that this does not really show why the problem that lifetime specifiers help solve.

Consider a different function with the same signature:

fn split_first(x: &str, y: &str) -> &str {
    if let Some(i) = x.find(y) {
        &x[..i]
    } else {
        x
    }
}

In this case, the output lifetime depends only on the lifetime of x, not y. The correct signature for this function is:

fn split_first<'a>(x: &'a str, y: &str) -> &'a str

If we also require y to be borrowed for the same lifetime, the code will still compile, but it is now more restrictive than it should be:

fn split_first<'a>(x: &'a str, y: &'a str) -> &'a str

There isn’t a simple mechanical rule that will let the compiler (or the reader of the code) know which lifetimes should match just by looking at the elided function signature. There are many possible correct answers depending on the programmer’s intent. Therefore, we ask the programmer to explicitly state that intent.

4 Likes

I understand that the more restrictive requirement is unnecessary, but I don't yet see how or when that would create a real problem.

I guess I fail to see how either of the input parameters can have it lifetime ended before the returned value goes out of scope.

Here’s a program that compiles with the “correct” split_first signature, but not with the overly restrictive one:

fn get_record_separator() -> String {
    std::env::var("RECORD_SEPARATOR").unwrap_or(" ".to_string())
}

fn first_record(records: &str) -> &str {
    let sep = get_record_separator();
    split_first(records, &sep)
}

Playground

4 Likes

It's on the caller side, imagine you had code like this:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    drop(string1); // borrow checker will flag the call below because of this
    println!("The longest string is {result}");
}

fn longest(x: &str, y: &str) -> &str { /* as in OP */ }

I'd guess that rust technically could default to the most restictive lifetime in all cases. It's not what it does as the designers prefered to have code explicit rather then implicitly choose one of multiple options. (something that you'll see quite often as a design choice in rust)

In case this is the confusion: Rust lifetimes ('_) in types are generally about how long some place like a variable is borrowed, and not the variable's liveness or drop scope.[1] And you usually want things to get borrowed for the shortest duration possible, to minimize the restrictions on what your other code can do.

Here's another example. In the version that compiles, the lifetime of the type of the &two that was passed to first ends before the push_str, but the (lifetime in the type of the) returned value is alive beyond that.

In the versions that don't compile, there's a use after the push_str that needs the lifetime to stay alive (because the input lifetime is tied to the lifetime of result in those cases). But two being borrowed conflicts with taking a &mut to call push_str; hence you get an error.


  1. Dropping conflicts with being borrowed, but so does just taking a fresh &mut, and other uses too; being destructed is just one possible conflicting use. ↩︎

2 Likes

No, why? You don't have to implement the function by returning both arguments. You can return both, only one of them, or neither.

Exactly! the function could return something completely different (unrelated to x or y)

here it is clear that x or y is chosen and no lifetime needed.

    let x = "short";
    let y = "longer";
    let longest = if x.len() > y.len() { x } else { y };

with the longest function it is not, looking at only the signature of the function.

I think this was the missing piece. Though I think in my example, instead of calling drop I'll assign a new value to string1. (for which I'll have to make it mutable as well.

Thank you!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.