Hi! A while ago a proposed to create a lint for a case where lifetime elision might be wrong, but the idea was rejected. Today the issue crossed my mind again, and I still think that the lint is worthwhile, so I’d like to discuss it a bit more
So, consider this Rust code:
#[derive(Copy, Clone)]
struct Holder<'a> {
data: &'a String
}
impl<'a> Holder<'a> {
fn data(&self) -> &String {
self.data
}
}
It compiles cleanly, without any errors or warnings. If you try to use it, you may even think that it works correctly. The following code compiles fine, after all:
fn f1(h: Holder) -> bool {
h.data().contains("Hello")
}
However, problems come if you try to use two holders simultaneously:
fn f2(h1: Holder, h2: Holder) -> & str {
if true {
&h1.data()
} else {
&h2.data()
}
}
error[E0106]: missing lifetime specifier
--> src/main.rs:17:34
|
17 | fn f2(h1: Holder, h2: Holder) -> & str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `h1` or `h2`
You may try to add a single lifetime to both holders and the return type:
fn f2<'h>(h1: Holder<'h>, h2: Holder<'h>) -> &'h str {
if true {
&h1.data()
} else {
&h2.data()
}
}
And it gives you a bunch of lifetime errors:
error[E0597]: `h1` does not live long enough
--> src/main.rs:19:10
|
19 | &h1.data()
| ^^ does not live long enough
...
23 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the lifetime 'h as defined on the function body at 17:1...
--> src/main.rs:17:1
|
17 | / fn f2<'h>(h1: Holder<'h>, h2: Holder<'h>) -> &'h str {
18 | | if true {
19 | | &h1.data()
20 | | } else {
21 | | &h2.data()
22 | | }
23 | | }
| |_^
error[E0597]: `h2` does not live long enough
--> src/main.rs:21:10
|
21 | &h2.data()
| ^^ does not live long enough
22 | }
23 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the lifetime 'h as defined on the function body at 17:1...
--> src/main.rs:17:1
|
17 | / fn f2<'h>(h1: Holder<'h>, h2: Holder<'h>) -> &'h str {
18 | | if true {
19 | | &h1.data()
20 | | } else {
21 | | &h2.data()
22 | | }
23 | | }
| |_^
Crucially, all these error messages are wrong, because the actual problem is not inside f2
, it’s the signature of get_data
.
If we specify lifetimes manually, we’ll get
impl<'a> Holder<'a> {
fn data<'b>(&'b self) -> &'b String
}
while the correct signature is
impl<'a> Holder<'a> {
fn data<'b>(&'b self) -> &'a String
}
Here, lifetime elision is wrong, because it prefers lifetime of Self, even though it is smaller than the lifetime of data
.
I think this is an important problem to fix because
- It happens in practice pretty often (personal experience).
- Error manifests itself some time after you’ve written the code: most simple functions work with smaller lifetime just fine.
- The error message is completely misleading, it points to the wrong bit of code.
- It’s not me who made the error, it’s the compiler who chose the wrong lifetime.
And I think this can be easily fixed with a lint, without any changes to the actual elision rules:
If the lifetime in the return position is elided and if the actual (inferred) lifetime is strictly greater than the elided one, produce a warning saying:
Potentially wrong lifetime elision: the signature without elision is
fn data<'b>(&'b self) -> &'b String
but the signature
fn data<'b>(&'b self) -> &'a String
also works and more general. Provide explicit lifetimes to silence this warning.
The reported issue is https://github.com/rust-lang/rust/issues/42287.
Do you think that this problem is important to fix? What are other possible solutions? What are the drawbacks of the lint?