-
Start with
#![deny(elided_lifetimes_in_paths)]
at the root of yoursrc/{lib,main}.rs
file. -
From there, it should lint about the missing lifetime parameter in, for instance,
render
's signature, expecting something like:fn render (mut f: impl FnMut(&mut RenderPass<'_>))
-
Now to apply the rules of lifetime elision:
-
Identify the lifetime parameters / placeholders / "holes":
// v vv fn render (mut f: impl FnMut(&mut RenderPass<'_>))
i.e.,
// vv vv fn render (mut f: impl FnMut(&'_ mut RenderPass<'_>))
-
Find the closest "function-signature boundary" surrounding those parameters. These can be:
-
The
fn render
function itself; -
fn(…) -> …
pointers -
impl
ordyn
Fn{,Mut,Once}(…) -> …
traits
-
-
And, "at that level", for each placeholder in function-input position, introduce new and distinct generic lifetime parameters / names.
In our case, the "level" is
FnMut
:// pseudo-code! fn render ( mut f: impl FnMut<'_0, '_1> (&'_0 mut RenderPass<'_1>), )
In the case of traits or
fn(…) -> …
pointers, these generics are actually written usingfor<…>
syntax:// Real code! fn render ( mut f: impl for<'_0, '_1> FnMut(&'_0 mut RenderPass<'_1>), )
-
These are called Higher-Rank Trait Bounds (see also Lifetime Quantification and Higher-Ranked Trait Bounds — Infinite Negative Utility), and are used to express an infinite sum/family of bounds; the bound applies for every possible choice of each lifetime parameter:
for<'_0, '_1> Stuff<'_0, '_1>
// is the same as
Stuff<'static, 'static> +
Stuff<'static, 'a> +
Stuff<'static, 'b> +
Stuff<'static, '…> +
Stuff<'a, 'static> +
Stuff<'a, 'b> +
Stuff<'a, '…> +
Stuff<'b, 'static> +
Stuff<'b, 'b> +
Stuff<'b, '…> +
Stuff<'…, 'static> +
Stuff<'…, 'b> +
Stuff<'…, '…> +
In our case, for<'_0, '_1> FnMut(&'_0 mut RenderPass<'_1>)
thus includes a bound such as:
FnMut(&'whatever mut RenderPass<'static>)
This is how 'static
managed to creep into your code .
To fix this, back to the pseudo code, remember that you had:
fn render (
mut f: impl FnMut<'any, …> (&mut RenderPass<'any>),
)
where 'any
had that universal / higher-level / higher-quantification that meant that it included the 'any = 'static
requirement: the caller has no say in this!
What the caller can choose are generic parameters at the level of the function itself (here, render
):
-
higher-order generic lifetimes: universal, not chosen by the caller!
- Often these are needed when the lifetime is callee-chosen
-
"normal" generic lifetimes: chosen by the caller
So we want to do:
fn render<'caller_chosen> (
mut f: impl FnMut(&mut RenderPass<'caller_chosen>),
)
and this does remove the 'static
requirement.
We thus reach the following
which does compile at the call-site, but now the callee-site fails with:
error[E0597]: `encoder` does not live long enough
--> src/main.rs:31:20
|
28 | fn render<'caller_chosen>(mut f: impl FnMut(&mut RenderPass<'caller_chosen>))
| -------------- lifetime `'caller_chosen` defined here
...
31 | let mut pass = encoder.pass();
| ^^^^^^^^^^^^^^
| |
| borrowed value does not live long enough
| assignment requires that `encoder` is borrowed for `'caller_chosen`
...
36 | }
| - `encoder` dropped here while still borrowed
So here it turns out that the higher-order lifetime was warranted / we did need to let the callee pick its own "arbitrary"(ly small) lifetime.
- We will need to go back to
for<'encoder>
lifetime parameters.
But when we did do that, Rust complained about an arbitrarily big lifetime ('static
)!
Well, now we know that we want to allow for arbitrarily low / small lifetimes! And it just happens that the naïve / default for<'any>
syntax…
-
requires support for arbitrarily small lifetimes (good for the callee)
-
while also requiring support for arbitrarily big lifetimes (bad for the caller!)
So we just need to opt out of the latter.
In an ideal world, this would be written as:
// pseudo-code!
fn render<'upper_bound, F> (mut f: F)
where
// 'upper_bound ⊇ 'encoder
for<'encoder where 'upper_bound : 'encoder>
F : FnMut(&mut RenderPass<'encoder>)
,
Sadly we cannot yet introduce explicit bounds for higher-order lifetimes; we'll thus need to ruse and abuse the fact that we can use implicit bounds!
fn render<'upper_bound, F>(mut f: F)
where
for<'enc>
F : FnMut(&mut RenderPass<'enc>, [&'enc &'upper_bound (); 0])
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// HACK: this empty array parameter,
// akin to `()` or `PhantomData`,
// carries no runtime data.
// But in the type-system, at compile-time,
// the ref represents a type which is
// only valid when the inner lifetime
// is bigger than the outer one;
// i.e., mentioning this type yields
// a `'upper_bound : 'enc` bound
,
{
let mut encoder = Encoder {};
let mut pass = encoder.pass();
f(&mut pass, []);
drop(pass);
}
-
Playground
-
This quirk may be more readable by defining a
type PhantomBound<'big, 'small> = PhantomData<&'small &'big ()>;
and then write the signature as:
for<'enc> F : FnMut(&mut RenderPas<'enc>, PhantomBound<'upper_bound, 'enc>) ,
Improving the ergonomics
If you don't want that extra quasi-vestigial parameter to be that visible in the closure, if you control the definition of RenderPass
, you can embed the 'upper_bound
inside it, which yields the following API (which does compile!):
fn main ()
{
let value = 1;
render(|pass| {
pass.set(&value);
});
}
fn render<'upper_bound, F>(mut f: F)
where
for<'enc>
F : FnMut(&mut RenderPass<'enc, 'upper_bound>)
,
// or just:
F : FnMut(&mut RenderPass<'_, 'upper_bound>),
{
let mut encoder = Encoder {};
let mut pass = encoder.pass();
f(&mut pass);
drop(pass);
}
The implementation of it is then just:
#![deny(elided_lifetimes_in_paths)]
struct Encoder {}
impl Encoder {
- pub fn pass<'a>(&'a mut self) -> RenderPass<'a> {
+ pub fn pass<'a>(&'a mut self) -> RenderPass<'a, 'static> {
RenderPass { parent: self, _upper_bound: <_>::default() }
}
}
- struct RenderPass<'a> {
+ struct RenderPass<'a, 'upper_bound : 'a> {
parent: &'a Encoder,
+ _upper_bound: ::core::marker::PhantomData<&'upper_bound ()>,
}
- impl<'a> RenderPass<'a> {
+ impl<'a> RenderPass<'a, '_> {
pub fn set(&mut self, _x: &'a i32) {}
}
- EDIT: this is the approach since taken by the standard library itself in their scoped threads API: notice their
'env
upper bound on the higher-order'scope
.