TL,DR
"Fixed" lifetimes in argument position, can be "problematic" and universal (HRTB) lifetimes ought to be used.
Instead of:
Box<dyn Fn(&'a String)>
you should use:
-
Box<dyn Fn(&'_ String)>where
'_stands for an elided lifetime parameter, which in this case representes an universally quantified lifetime (HRTB):Box<dyn for<'any> Fn(&'any String)>
To see why, let's start from:
pub
struct MessageHandler;
impl<'arg> BoxFn<'arg> for MessageHandler {
type Arg = String;
fn boxit<F : 'static> (f: F) -> Box<dyn Fn(&'arg String)>
where
F : Fn(&'_ String), // for<'any> F : Fn(&'any String)
{
Box::new(move |arg| {
f(arg);
})
}
}
/// F = fn { handler }
/// for<'any> F : Fn(&'any String)
fn handler (message: &'_ String)
{
println!("Message is {}", message);
}
fn main ()
{
let boxed_fn = MessageHandler::boxit(handler); // Box<dyn Fn(&'arg String)>
let message = "Test".to_string();
boxed_fn(&message);
}
- I have taken the liberty to rename
'ato'argto make it more readable, and removed the&'staticindirection over thef: Fargument, since it is not needed (F : 'staticsuffices for your signature).
So, now the question is:
what is
'arg?
how far does'arggo / span?
Indeed, any lifetime parameter represents a lifetime, which can be seen as a span of the program.
Finding / choosing the exact lifetime span is Rust's job: an API bounds lifetimes, either by forcing them to reach some point, or by forbidding them from going further than some point. Rust then tries to find some "code section" / span that matches the given bounds, or triggers a compile error when it fails.
And that's what happens with your code:
-
boxed_fn : Box<dyn Fn(&'arg String)>is a variable whose type carries a'arglifetime. This means that'argcannot have ended whenboxed_fnis used.⟹
'argmust span beyond the last usage ofboxed_fn-
Thanks to NLL (borrow checker 2.0 of sorts), the last usage point does not necessarily correspond to the point where the variable
boxed_fndisappears. But when a variable has drop glue / a destructor, which is the case ofBox<_>(the destructor needs to deallocate), then by definition the variable is used right before disappearing. This means that when the type has drop glue, NLL "does not" apply, and instead the old rules take place.⟹
'argmust span untilboxed_fnis dropped.
-
-
Now,
'argis also the lifetime of the borrow of theStringthat is fed toboxed_fn(that's whatdyn Fn(&'arg String)means). And by definition a value cannot be borrowed beyond the point where it is dropped:⟹
'argmust end beforemessageis dropped.
And in the code:
let boxed_fn = MessageHandler::boxit(&handler);
let message = "Test".to_string();
boxed_fn(&message);
}
it is just not possible to meet both requirements. To better see it, know that variables are dropped in reverse order of their declaration. This means that the above code is equivalent to:
let boxed_fn = MessageHandler::boxit(&handler);
let message = "Test".to_string();
¤ boxed_fn(&message);
¤
drop(message);
drop(boxed_fn);
~
~
}
-
¤represents the span of "end beforemessageis dropped" (but after the borrow starts); -
~represents the span of "untilboxed_fnis dropped";
As you can see, these spans do not overlap, hence the error (if they overlapped then Rust would be free to choose any point in the intersection and the code would compile). By the way, by writing down the drops explicitely, the error Rust shows is a little bit more explicit too:
error[E0505]: cannot move out of `message` because it is borrowed
--> src/main.rs:41:10
|
40 | boxed_fn(&message);
| -------- borrow of `message` occurs here
41 | drop(message);
| ^^^^^^^ move out of `message` occurs here
42 | drop(boxed_fn);
| -------- borrow later used here
- An example of the lifetimes overlapping can be seen if we invert the
droporder:
let message = "Test".to_string();
let boxed_fn = MessageHandler::boxit(&handler);
¤ boxed_fn(&message);
¤ drop(boxed_fn);
¤~ // <--- 'arg can end here --+
~ drop(message);
}
The solution
By making the type of boxed_fn not carry an explicit lifetime, while still being "callable", the problem is solved.
And a way to have a "callable with an argument borrowed for some lifetime" interface without infecting the whole Box<dyn ...> type with that specific lifetime, is to just use a non-specific lifetime. Such a non-specific lifetime is "universally quantified" instead, meaning that the callable interface (and bound) does not apply to a specific borrow but to all possible borrows (hence the "universal").
This is what is called a HRTB, and can be written either with the explicit "for all" syntax:
Box<dyn for/*all*/<'borrow> Fn(&'borrow String)>
or, when dealing with Fn{,Mut,Once} traits specifically, it can be written with the "elided lifetime parameter" syntax, i.e., writing down the '_ elided lifetime parameter (explicit elision), or just not writing it at all (implicit elision):
Box<dyn Fn(&'_ String)>
Box<dyn Fn(&String)>
So the following BoxFn trait signature solves your problem:
pub
trait BoxFn {
type Arg;
fn boxit<F : 'static> (f: F) -> Box<dyn Fn(&'_ Self::Arg)>
where
F : Fn(&'_ Self::Arg), // for<'any> F : Fn(&'any Self::Arg),
{
Box::new(f)
}
}