Unlikely place for lifetimes to present an issue

Here's a simple example using trait objects.

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn new() -> Self {
        Screen { components: vec![], }
    }

    pub fn draw_all(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }

    pub fn add_component(&mut self, component: impl Draw) {
        self.components.push(Box::new(component));
    }
}

I get the following error:

error[E0310]: the parameter type `impl Draw` may not live long enough
  --> src/main.rs:22:30
   |
22 |         self.components.push(Box::new(component));
   |                              ^^^^^^^^^^^^^^^^^^^ ...so that the type `impl Draw` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
21 |     pub fn add_component(&mut self, component: impl Draw + 'static) {
   |                                                          +++++++++

That seems like a strange place to require a `static lifetime. I'm not even trying to pass the component as a reference, but rather by passing ownership of a value that implements Draw, so why are lifetimes even required here?

I found the following related topic, but it still doesn't explain why lifetimes are even required in my example. I thought lifetimes were only relevant for references, and not when passing by value.

Without the ’static bound there’s no guarantee that the argument doesn’t contain any references. You could impl Draw for &’_ MyStruct { … }, for instance, and then pass an &MyStruct as component.

2 Likes

For clarity, if I use the following syntax, I still get the same error:

    pub fn add_component<T>(&mut self, component: T) where T: Draw {
        self.components.push(Box::new(component));
    }

It should be even clearer with this example that the component is not provided as a reference, so again, why are lifetimes even required here?

References in Rust are fully-fledged types, and so can be substituted for a generic type parameter without being called out explicitly. If you don’t want that to be possible, you need to add a T: ’static bound, which is how you tell the compiler that T cannot have any lifetime-related restrictions. In practice, this means it will contain no references (except possibly for &’static ones, which are generally not a problem).

2 Likes

Very interesting. Thank you for pointing me in the right direction.

Rather than require a 'static lifetime, I tried the following, which parameterizes the Screen with an explicit lifetime parameter, and then requires all contained components to have at least that lifetime.

Of course, this will only be an issue if any components use references, as your example describes. At least this way it seems to make the requirement as generic as possible.

pub trait Draw {
    fn draw(&self);
}

pub struct Screen<'a> {
    components: Vec<Box<dyn Draw + 'a>>,
}

impl<'a> Screen<'a> {
    pub fn new() -> Self {
        Screen { components: vec![], }
    }

    pub fn draw_all(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }

    pub fn add_component(&mut self, component: impl Draw + 'a) {
        self.components.push(Box::new(component));
    }
}

Thankfully, this solution doesn't require any explicit mention of lifetimes when using Screen. The lifetime constraints are naturally inferred.

struct Circle {
    x: i32,
    y: i32,
    radius: i32,
}

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing circle at {},{} - radius {}", self.x, self.y, self.radius);
    }
}

fn main() {
    let mut screen = Screen::new();

    let circle = Circle { x: 15, y: 10, radius: 6 };
    screen.add_component(circle);

    screen.draw_all();
}

Does anyone have any recommendations as to whether my parameterized solution above is a good design pattern, or should I simply use the 'static requirement?

The former would allow inter-component references, as long as they can be guaranteed to live as long as the Screen itself.

The latter would communicate the intent that all components stored to be displayed on the Screen must contain no references (or at least only static ones, such as &'static str).

Is this kind of flexibility a common Rust pattern, when using something like the Screen here to act as an "arena" to store the various components it contains, and avoid the use of Rc and friends? I'd especially appreciate any pointers to further reading on this subject.

What should happen when you substitute T = &U, for some type U? A bare type variable T doesn't mean "any type but not a reference", it means "any type".

If you can make your code compile reasonably easily with a lifetime parameter, and you need to support references, then go for it. Adding the 'static bound would mean that your code isn't practically usable with regular, short-lived references.

Sometimes, going with a 'static trait object is the more practical approach, because lifetimes would be infectious otherwise. That can be undesirable/annoying/too complicated to be worth the effort. However, in simple cases, it's nice to provide the flexibility, so that code is more widely usable.

4 Likes