Why does implementing a trait change the behaviour of lifetimes?

Viewed 30

This is the smallest thing I could get that reproduces my problem:

pub trait IoEventHandling {
fn add_key_callback(&mut self, callback : Box<dyn FnMut(&[KeyActionState])>);
}

pub struct KeyActionState;

type KeyEvent<'io> = Box<dyn FnMut(&[KeyActionState]) + 'io>;

struct Window<'io> {
    key_callbacks: Vec<KeyEvent<'io>>,
}

// impl<'io> Window<'io> {
impl<'io> IoEventHandling for  Window<'io> {
    fn add_key_callback(&mut self, callback: KeyEvent<'io>) {
        self.key_callbacks.push(callback);
    }
}

struct PeripheralBureau<'io> {
    window: Window<'io>,
}

impl<'io> PeripheralBureau<'io> {
    fn window(&mut self) -> &mut Window<'io> {
        &mut self.window
    }
}

struct ECStorage;

impl ECStorage {
    fn component_query<T>(&mut self) -> impl Iterator<Item = ()> {
        std::iter::empty()
    }
}

struct KeyBoardMotion;

fn move_user_controlled_entities<'io>(
    ecs: &'io mut ECStorage,
    io_context: &mut PeripheralBureau<'io>,
) {
    io_context
        .window()
        .add_key_callback(Box::new(move |states| {
            for motion in ecs.component_query::<KeyBoardMotion>() {}
        }))
}

If you comment out impl<'io> IoEventHandling for Window<'io> { and uncomment the line above it it will work.

Why is implementing the trait breaking the code?

1 Answers

Your trait has a more strict requirement than the implementation. Namely, it requires callback to be Box<dyn FnMut(&[KeyActionState])>, which is (implicitly) Box<dyn FnMut(&[KeyActionState]) + 'static>; and therefore, when you call the trait method, you're required to satisfy this 'static bound - in other words, you're required to provide callback which can be held alive by the implementor indefinitely long.

When implementing add_key_callback as an inherent method, however, you're lifting this restriction be making it dependent on the struct's lifetime - therefore, callback must simply outlive the struct.

To make this work with the trait, you have to make the trait itself generic, so that each Window<'io> for different lifetime gets essentially its own impl<'io> IoEventHandling<'io>, and these implementations will require different lifetimes on the argument, therefore avoiding constraining it too hard.

Related