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
SomeField
necessarily 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] self
method 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
Value
is to be "connected" to theself
receiver; andkey
, on the other hand, is in a "I'm not borrowed" kind of situation. -
⇒
key
will thus be using its own distinct "dummy" lifetime, whereasself
and the returnedValue
will 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 Traits
1. 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 '_ + Trait
it 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 Traits
with 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
A
orB
, whereas in the former case this will be left for type inference.