Lifetime elision
- Prerequisite: elided lifetimes.
The lifetime elision rules are just there as convenience sugar for what could otherwise be written in a more verbose manner. As with any sugar that does not want to become overly terse and footgunny (cough, C++, cough), it only makes sense to have it in clear unambiguous cases.
Lifetime elision in function bodies
Inside function bodies, that is, in a place where type inference is allowed, any elided lifetimes, implicit or not, will adjust to become whatever suits the code / whatever type-inference (with borrow-checking) dictates.
Lifetime elision in function signatures
By "function signatures" the following rules will apply to:
-
A function (item) signature:
-
fn some_func(…) -> …
-
-
A function pointer type:
-
type MyCb = fn(…) -> …; // ^^^^^^^^^^
-
-
A
Fn{,Mut,Once}trait bound:-
: Fn{,Mut,Once}(…) -> … -
impl Fn{,Mut,Once}(…) -> … -
dyn Fn{,Mut,Once}(…) -> …
-
So, for all these, it turns out there are only two categories of "clear unambiguous cases":
-
When lifetimes play no meaningful role
That is, when the function returns nothing that borrows or depends on inputs. That is, when the return type has no "lifetime" parameters:
#![allow(unused)] fn main() { fn eq(s1: &'_ str, s2: &'_ str) -> bool // <- no "lifetimes"! }Since there isn't really any room for lifetimes/borrows subtleties here, the unsugaring will be a maximally flexible one. Given that repeating a lifetime parameter name is a restriction (wherein two lifetime-generic types will need to be using equal "lifetimes" / matching regions), we get to be maximaly flexible / lenient / loose by not doing that: by introducing and using distinct lifetime parameters for each lifetime placeholder:
#![allow(unused)] fn main() { fn eq<'s1, 's2>(s1: &'s1 str, s2: &'s2 str) -> bool } -
With borrowing getters
With thus a clear unambiguous receiver / thing being borrowed:
-
#![allow(unused)] fn main() { fn get(from_thing: &'_ Thing) // same for `BorrowingThing<'_>`. -> &'_ SomeField // replacing `&'_` with `&'_ mut` works too, of course. }-
In case of single "lifetime" placeholder among all the inputs;
-
⇒ the borrow of
SomeFieldnecessarily stems from it; -
⇒ the output is to be "connected" to that input borrow by repeating the lifetime parameter.
fn get<'ret> (from_thing: &'ret Thing) -> &'ret SomeField
-
-
#![allow(unused)] fn main() { fn get(&'_ self, key: &'_ str) -> &'_ Value }-
A
&[mut] selfmethod receiver, despite the multiple lifetime placeholders among all the inputs, is "favored" and by default deemed to be the "thing being borrowed". -
⇒ the borrow of the
Valueis to be "connected" to theselfreceiver; andkey, on the other hand, is in a "I'm not borrowed" kind of situation. -
⇒
keywill thus be using its own distinct "dummy" lifetime, whereasselfand the returnedValuewill be using a single/repeated lifetime parameter.fn get<'ret, '_key> ( self: &'ret Self, key: &'_key str, ) -> &'ret Value
-
-
Regarding fn-pointers and Fn… traits, the same rules apply, it's just that the named "generic" lifetime parameters for these things have to be introduced as "nested generics" / higher-rank lifetime parameters, using the for<> syntax.
-
In the
fn-pointer case, we have:let eq: fn(s1: &'_ str, s2: &'_ str) -> bool; // stands for: let eq: for<'s1, 's2> fn(s1: &'s1 str, s2: &'s2 str) -> bool; -
In the
Fn…case, we have:impl/dyn Fn…(&'_ str, &'_ str) -> bool> // stands for impl/dyn for<'s1, 's2> Fn…(&'s1 str, &'s2 str) -> bool
So the rule of thumb is that "lifetime holes in ouput connect with an unambiguous lifetime hole in input".
-
Technically, lifetime holes can connect with named lifetimes, but mixing up named lifetimes with elided lifetimes for them to "connect" is asking for trouble/confusing code: it ought to be linted against, and I hope it will end up denied in a future edition:
fn example<'a>( a: impl 'a + AsRef<str>, _debug_info: &'static str, ) -> impl '_ + AsRef<str> { a // Error, `a` does not live for `'static` }
Lifetime elision in dyn Traits
Yep, yet another set of rules distinct from that of elision in function signatures, and this time, for little gain 😔
- tip: if you're the one writing the Rust code, avoid using these to reduce the cognitive burden of having to remember this extra set of rules!
This is a special form of implicit elision: dyn Traits1. Indeed, behind a dyn necessarily lurks a "lifetime"/region: that of owned-usability.
dyn Traits // + 'usability?
-
⚠️ this is different than the explicitly elided lifetime,
'_.That is, if you see a
dyn '_ + Traitit will be the rules for lifetime elision in function signatures which shall govern what'_means:fn a(it: Box<dyn Send>) // :impl Fn(Box<dyn 'static + Send>) fn b(it: Box<dyn '_ + Send>) // :impl for<'u> Fn(Box<dyn 'u + Send>)
When I write Traits, it is expected to cover situations such as Trait1 + Trait2 + … + TraitN
Rules of thumb for elision behind dyn:
-
Inside of function bodies: type inference.
-
&'r [mut] dyn Traits = &'r [mut] (dyn 'r + Traits)- More generally, given
TyPath<dyn Traits>whereTyPath<T : 'bound>, we'll haveTyPath<dyn Traits> = TyPath<dyn 'bound + Traits>.
- More generally, given
-
otherwise
dyn Traits = dyn 'static + Traits
Lifetime elision in impl Traits
Return-position -> impl Traits (RPIT)
Similarly to dyn, here, -> impl Traits ≠ -> impl '_ + Traits.
But contrary to other properties/aspects of type erasure (where dyn and -> impl can be quite similar), when dealing with lifetimes / captured generics, impl Traits happens to involve different semantics than those of dyn Traits ⚠️
See the dedicated section for more info.
Argument-position impl Traits (APIT)
- (dubbed "universal" / caller-chosen)
Intuition: impl Traits will "be the same as impl '_ + Traits", that is, introducing a new named generic lifetime parameter <'usability>, and then impl 'usability + Traits. This aligns with the idea of it being a "universal" impl type.
Rationale / actual semantics
These are kind of analogous to the following:
For each impl Traits occurrence,
- introducing a new generic type parameter:
<T> - bounded by the
Traits:where T : Traits - and replacing the
impl Traitswith that parameter:T.
fn example(a: impl Send, b: impl Send)
// is the "same" as:
fn example<A, B>(a: A, b: B)
where
A : Send,
B : Send,
- the only difference will be that in the latter signature, callers will be able to use turbofish to specify the actual choices of
AorB, whereas in the former case this will be left for type inference.