I've a general question regarding handling (potential) trait objects that contain methods with generics and then subsequently hiding them in another object. I am aware that traits that contain methods with generics are not object safe and can not be made into a formal trait object. Generally, my strategy for handling this is to just create a generic and restrict the type to those that implement the trait. Where this becomes more burdensome is when this type gets hidden in another object such as a closure. At this point, there's a lifetime problem where it's important that the lifetime of the object not exceed the lifetime of the type. Normally, I handle this in one of three ways: requiring the type to have a static lifetime, matching the lifetime of the type to that of the object, or just requiring the trait itself to be static. My question is whether or not there's a better strategy for handling this. At times, I have quite a few objects where this issue occurs, which leads to quite a few, understandable, lifetime annotations. This leads to complicated and burdensome function calls. As such, I've started more commonly just requiring the trait itself to be static. Is there a better way to handle this?
Here's some code demonstrating the issue and the current resolutions:
// Trait with a method dependent on a generic
trait Foo {
fn foo<T>(&self, t: T);
}
// An object that implements foo
struct Bar;
impl Foo for Bar {
fn foo<T>(&self, _t: T) {
println!("I am a Bar");
}
}
// Note, functions can't take Foo as a trait object due to foo having a generic
//fn not_trait_object(x : &dyn Foo) {}
// To get around the trait object limitation, we pass in a generic
fn trait_object_workaround<F>(x: F)
where
F: Foo,
{
x.foo(0);
}
// If we try to hide a Foo inside of an object that does not depend on a
// generic that requires Foo, we get a lifetime problem
//fn lifetime_issue_hiding_object<F>(x: F) -> Box<dyn Fn() -> ()>
//where
// F: Foo,
//{
// Box::new(move || {
// x.foo(0);
// })
//}
// One resolution to this is matching the lifetime of the closure to that of
// type F. This adds another paramter to the function.
fn lifetime_matching_on_type<'a, F>(x: F) -> Box<dyn Fn() -> () + 'a>
where
F: Foo + 'a,
{
Box::new(move || {
x.foo(0);
})
}
// Alternatively, we could just force F to be static. This saves the extra
// lifetime parameter.
fn static_requirement_on_type<F>(x: F) -> Box<dyn Fn() -> ()>
where
F: Foo + 'static,
{
Box::new(move || {
x.foo(0);
})
}
// As a last alternative, we could just add the requirement that anything
// implementing Foo be static. This makes the trait less general, but saves on
// the lifetime issues that require attention later.
trait FooStatic: 'static {
fn foo<T>(&self, t: T);
}
impl FooStatic for Bar {
fn foo<T>(&self, _t: T) {
println!("I am a Bar static");
}
}
fn static_on_the_trait<F>(x: F) -> Box<dyn Fn() -> ()>
where
F: FooStatic,
{
Box::new(move || {
x.foo(0);
})
}
// Test some of the functions
fn main() {
trait_object_workaround(Bar);
lifetime_matching_on_type(Bar)();
static_requirement_on_type(Bar)();
static_on_the_trait(Bar)();
}
Thanks for the help!