While writing previous post on “Rust lifetimes”, for brevity reasons I’ve decided to exclude details about lifetime elision and rather keep it for the next post.
If you have followed previous post on “Rust borrowing”, you most likely noticed that in some code snippets there were functions that accepted multiple references as parameters but did not require to explicitly annotate lifetimes. To assure you, lifetimes are always there. Rust never forgets about lifetimes but rather allows to omit annotation syntax when it is obvious what it is. Rust has something called lifetime elision rules. It is somewhat similar to type inference in strongly typed languages such as Swift or Kotlin but with some limitations.
In very simple, deterministic cases, where function takes arbitrary number of references as parameters but does not return one as such:
fn sum(x: &i32) -> i32
// or
fn sum(x: &i32, y: &i32) -> i32
// and so on
You don’t need to explicitly annotate parameter lifetimes. Rust does that for you behind the scenes by providing each reference its own input lifetime. Equivalent sum
function will look like this:
fn sum<'a>(x: &'a i32) -> i32
// And in case there are multiple references
fn sum<'a, 'b>(x: &'a i32, y: &'b i32) -> i32
// and so on
In other unambiguous cases, where function takes a single reference as a parameter and returns a reference as such:
fn sum(x: &i32) -> &i32
Rust assigns the same lifetime for both: input and output references (namely input lifetime and output lifetime). sum
function will look something like this behind the scenes:
fn sum<'a>(x: &'a i32) -> &'a i32
So far it is all nice and dandy, but what will happen if we try multiple reference parameters and return reference as well, as such:
fn sum(x: &i32, y: &i32) -> &i32
You most likely guested it: at this point Rust cannot unambiguously determine lifetimes of each reference. Rust lifetime elision will set input lifetime parameters as follows:
fn sum<'a, 'b>(x: &'a i32, y: &'b i32) -> &i32
However, it is impossible for Rust compiler to figure out the lifetime of the returned reference. That’s why compiler will complain and ask to annotate lifetimes explicitly.
There’s one more interesting rule. So far we only addressed functions but in case there is an instance method (fn
within impl
blocks) with multiple parameters, lifetime elision will automatically assign instance lifetime for the returned value reference, given &self
or &mut self
is provided, thus making the following method signature valid without explicit lifetime annotations:
impl Foo<'_> {
fn bar(&self, x: &i32, y: &i32) -> &i32
// works for `&mut self` as well
fn bar(&mut self, x: &i32, y: &i32) -> &i32
}
And equivalent with annotations:
impl<'a> Foo<'a> {
fn bar<'b, 'c>(&'a self, x: &'b i32, y: &'c i32) -> &'a i32
}
This drastically reduces amount of annotations and repetitive code being written again and again once more methods are added to the instance. In addition, it is important to know that if lifetime elision does not behave as you wanted, you can always add annotations explicitly.
Lifetime elision rules is a great initiative and effort from Rust team to make code less verbose. Even if it doesn’t provide full lifetime inference at this point as one might expect, it is plausible that when more deterministic patterns appear, they could be added to the Rust compiler.