why are lifetimes not required on generic functions
Asked Answered
K

2

7

This code won't compile because rust requires a lifetime to be added.

fn firstNoLifetime(x: &str, y: &str) -> &str {
    return x;
}

So instead we must add the lifetime explicitly like this:

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

My question then is, how is it, that this function, without lifetimes, compiles ?
Why are lifetimes not required for generic functions, and are there any caveats around that ?

fn first_generic<A>(x: A, y: A) -> A {
    return x;
}

My assumption was that lifetime annotations help the compiler and borrow checker determine the root cause of lifetime violations, but in the code below rust was able to determine the cause without annotations. Is my assumption wrong ? And if so, what is the purpose of lifetime annotations ?

fn first_generic<A>(x: A, y: A) -> A {
    return x;
}

fn main() {
    let string1 = String::from("long string is long");
    let result: &str;
    {
        let string2 = String::from("xyz");
        result = first_generic(string1.as_str(), string2.as_str());
    }

    println!("The first string is: {}", result);
}

result:

   |
10 |         result = first_generic(string1.as_str(), string2.as_str());
   |                                                  ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
11 |     }
   |     - `string2` dropped here while still borrowed
12 | 
13 |     println!("The first string is: {}", result);
   |                                           ------ borrow later used here
Krahling answered 3/4, 2022 at 2:58 Comment(7)
The short answer is that lifetimes are part of the type itself, and so are inferred from the call site. This is why you sometimes see lifetime bounds on generic types, e.g. where T: 'a.Longspur
why wouldn't they be inferred when the types are concrete though ?Stockmon
Because the spec has specific rules around lifetime elision while inference of generic arguments is a lot more general. In particular, note that in your generic example, T doesn't have to be a reference at all.Longspur
wouldn't the lifetime become required during generic instantiation for the &str type though ?Stockmon
Yes, but the caller provides all of T, including the lifetime.Longspur
Actually as you can see in my sample code, the lifetime is not provided at the call site, the generic and non generic call sites are identical.Stockmon
It's still provided by the caller, just automatically, by generic type inference at the call site.Longspur
H
1

Lifetimes are part of type itself

In your first example, you are specifying same i.e 'a to all of x, y and return value. If you were receiving single value as reference you wont have to pass lifetime specifier right?

fn first(x: &str) -> &str {
    x
}

It's because compiler infer the lifetime of both on it's own.

Back to your question, "why lifetimes are not required in generic function", short answer is yes they do. Just not in example you provided. Why?

In your second example, you are presenting that x, y and return value must have same type as all of they are A. Reminding that lifetime are part of type itself, here comes only one lifetime comes into play, so compiler can infer it on it's own.

My assumption was that lifetime annotations help the compiler and borrow checker determine the root cause of lifetime violations,

Yes your assumption is also correct. Lifetime annotation do help compiler & borrow checker in this case to determine root cause of lifetime violation.

but in the code below rust was able to determine the cause without annotations

As mentioned above, only one lifetime is in play here so rust was able to infer on it's own. However this was not true in certain older version of rust compiler ( I don't remember exact version but at that time, we had to specify lifetime annotation in your generic case too )

Higginson answered 3/4, 2022 at 3:33 Comment(0)
C
3

When lifetimes are elided in a function signature, each input lifetime becomes a distinct anonymous lifetime parameter. Thus, this:

fn firstNoLifetime(x: &str, y: &str) -> &str {
    return x;
}

is equivalent to this:

fn firstNoLifetime<'a, 'b>(x: &'a str, y: &'b str) -> &'? str {
    return x;
}

What's the lifetime for the return value, though? It doesn't make sense to use a distinct lifetime here, since no useful reference could satisfy that lifetime.

Per lifetime elision rules, when there's a single input lifetime, then that lifetime is used for the return value. If there is more than one input lifetime, but one of them is &self or &mut self, then the lifetime of self is used for the return value. The reason for these rules is that we don't want lifetime inference to be based on a function's implementation, because then a change in the implementation could lead to the signature changing, which will generally be a breaking change.

The reason the generic function works is because the lifetime parameter is part of the type of a reference. When you invoke first_generic with references, then the compiler has to find a single type that is compatible with both arguments (including finding a common lifetime), the same way it would have to for first. The error we get with firstNoLifetime cannot happen with first_generic because firstNoLifetime's parameters have different types, which cannot happen with first_generic because we specified that the two parameters have the same type A (whatever A is).

Collinsia answered 3/4, 2022 at 4:45 Comment(0)
H
1

Lifetimes are part of type itself

In your first example, you are specifying same i.e 'a to all of x, y and return value. If you were receiving single value as reference you wont have to pass lifetime specifier right?

fn first(x: &str) -> &str {
    x
}

It's because compiler infer the lifetime of both on it's own.

Back to your question, "why lifetimes are not required in generic function", short answer is yes they do. Just not in example you provided. Why?

In your second example, you are presenting that x, y and return value must have same type as all of they are A. Reminding that lifetime are part of type itself, here comes only one lifetime comes into play, so compiler can infer it on it's own.

My assumption was that lifetime annotations help the compiler and borrow checker determine the root cause of lifetime violations,

Yes your assumption is also correct. Lifetime annotation do help compiler & borrow checker in this case to determine root cause of lifetime violation.

but in the code below rust was able to determine the cause without annotations

As mentioned above, only one lifetime is in play here so rust was able to infer on it's own. However this was not true in certain older version of rust compiler ( I don't remember exact version but at that time, we had to specify lifetime annotation in your generic case too )

Higginson answered 3/4, 2022 at 3:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.