To understand the lifetimes in this code, let’s start out by addressing the lifetime elision that is going on.
If you have a struct with a lifetime argument like Event<'a>
, it is generally not advisable to ever write “Event
” without that lifetime argument, since that’s relying on old (and hopefully, eventually, deprecated) syntax for lifetime elision which does regularly and easily lead to confusion simply by the fact that there is any lifetime elision going on in the first place is very well-hidden.
We end up with elided lifetimes in three places, marked below:
use core::marker::PhantomData;
struct Event<'a> {
msg: &'a str,
}
struct Listener(i32);
impl Listener {
fn for_each(mut self, mut f: impl FnMut(Event<'_>)) { // <- here
for i in 1..10 {
let message = String::from("Hello, World!");
self.0 = i;
f(Event { msg: &message });
}
}
}
struct Engine {}
impl Engine {
fn foo(&mut self, listener: Listener, mut reader: Reader<Event<'_>>) { // <- here
let f = move |event: Event<'_>| reader.read(&event); // <- here
listener.for_each(f);
}
}
struct Reader<E>(PhantomData<E>);
impl<E> Reader<E> {
fn read(&mut self, _: &E) {}
}
By the way note that the line f(Event { msg: &message })
is different as “Event
” in this line is not the type Event
itself, but the value-constructor for Event
.
Next up, since lifetime elision can be confusing, especially since the meaning depends heavily on context, let’s de-sugar the elided Event<'_>
lifetimes into explicit ones. In one place that’s straightforward:
fn foo<'a>(&mut self, listener: Listener, mut reader: Reader<Event<'a>>) {
one place needs knowledge of the special elision rules for Fn
… traits,
fn for_each(mut self, mut f: impl for<'b> FnMut(Event<'b>)) {
and one place does, really, not allow any desugaring at all; but the meaning should be clear if we want to match the FnMut
signature; it’s a generic lifetime here as well, and we can use unstable features to spell it out if we want to be very explicit:
#![feature(closure_lifetime_binder)]
……
let f = for<'b> move |event: Event<'b>| -> () { reader.read(&event) };
(full code at this point in the playground)
Now to analyze the problem: It’s a simple type mismatch!
impl Engine {
fn foo<'a>(&mut self, listener: Listener, mut reader: Reader<Event<'a>>) {
let f = for<'b> move |event: Event<'b>| -> () { reader.read(&event) };
listener.for_each(f);
}
}
impl<E> Reader<E> {
fn read(&mut self, _: &E) {}
}
The reader.read
call for Reader<Event<'a>>
expects a reference to Event<'a>
, whereas the event
you pass a reference to is of type Event<'b>
with a different lifetime. The fact that the 'a
lifetime is one provided to foo<'a>
, and thus a lifetime that outlives the function, whereas event: Event<'b>
is contains a &'b str
reference that is potentially much more short-lived, inside of a closure, is the reason why we get the particular error message that we get.
error[E0521]: borrowed data escapes outside of closure
--> src/lib.rs:23:57
|
22 | fn foo<'a>(&mut self, listener: Listener, mut reader: Reader<Event<'a>>) {
| ---------- `reader` declared here, outside of the closure body
23 | let f = for<'b> move |event: Event<'b>| -> () { reader.read(&event) };
| ----- ^^^^^^^^^^^^^^^^^^^ `event` escapes the closure body here
| |
| `event` is a reference that is only valid in the closure body
Whether this error message would have been more helpful if it called out the lifetime mismatch more explicitly, is something that could be discussed.
As for how to solve the problem: There’s no good way after all, by just changing the lifetimes. Looking at your code you are passing very short-lived references to local String
variables with the listener.for_each(f);
call. Those references are already dead again, and the Strings
deallocated, after the call to .for_each
was finished. On the other hand (at least, I assume, based on the signature), Reader<E>
will probably assume to actually obtain some values of type E
that it can keep around for longer, which of course it cannot if the thing it actually gets passed is (containing) a short-lived reference that dies long before the Reader
goes out of scope.
There are 3 options I can think of how to go about solving the problem now:
- the
Reader
could possibly be living longer than it needs to, so so maybe foo
needs re-factoring of some sort (though in foo
alone I don't have any concrete ideas how to fully address the issue)
- the
Reader
type might actually not need to retain any E
values; perhaps, and maybe even likely, judging by the &E
type of the argument, it only need to be able to look at the value of type E
during the call to read
, but not retain any copies or anything. It’s not trivial to make this more generic so different, but related, types of E
are supported by a single Reader
– I don’t think I can give any good suggestions without seeing more than this minimal/toy example, from which alone it’s impossible to guess what Reader
actually does, and what kind of abstraction it represents / what types besides Event<…>
it’s used with, etc… E.g. perhaps the real code involves some trait bounds on E
, in which case that trait could possibly be re-worked to allow for usage generic over some lifetime. Also it isn’t clear whether E
only exists as PhantomData
in the real code, too, or whether actual E
values are being stored, which would be a strong indicator that perhaps moving to owned values (see below) is the only option…
- the usage of a borrowed
&str
might be unwanted. Perhaps you do want some owned value after all. Maybe a String
; maybe something shared like Arc<String>
or Arc<str>
, or something else