Forall lifetime for a struct (instead of a trait)

This code does not compile:

struct Signal<T> {
    handlers: Vec<Box<dyn Fn(&T)>>,
}

impl<T> Signal<T> {
    fn emit(&mut self, data: &T) {
        for handler in &self.handlers {
            handler(data);
        }
    }
}

struct Message<'a, T> {
    data: &'a T,
}

fn main() {
    let mut signal = Signal { handlers: Vec::new() };
    let data = 0; // can be any arbitrary data
    signal.emit(&Message { data: &data }); // error: borrowed value does not live long enough
}

signal is a Signal<Message<'a, i32>>, where 'a must last as long as signal. Can I somehow constrain 'a to the smallest possible lifetime, so it only lasts for each call to emit()? I'm trying to get a similar outcome to Fn(&T), where &T has the smallest possible lifetime.

What I really want is the type for<'a> Signal<Message<'a, i32>>, but of course Message is not a trait so that's not possible. Is it possible to express this some other way?

The problem seems to be that dyn Fn(&T) is invariant in T. If you use function pointers instead of boxed closures, then your code will compile (playground):

struct Signal<T> {
    handlers: Vec<fn(&T)>
}

I'm not sure if there's any trick that would extend this to stateful closures while keeping the type contravariant. It would need to somehow forbid closures that use internal mutability to copy the &T argument into their internal state.

EDIT: This is incorrect; see below.

It is not because of invariance. The original code failed because the type Signal<Message<'a, T>> is annotated with the lifetime 'a, and to pass a reference to the data variable to emit, the lifetime on signal must be the lifetime of data.

However, a struct must not outlive any lifetimes annotated on it, so the signal must be destroyed before the lifetime 'a ends, hence data must go out of scope after signal does. This compiles:

fn main() {
    let data = 0;
    let mut signal = Signal { handlers: Vec::new() };
    signal.emit(&Message { data: &data });
}

The function pointer works because of a special case in the drop-checker for types with simple destructors.

1 Like

For future reference, if you end up deciding you really need a universal quantifier inside your struct, this would be the syntax:

struct Signal<T> {
    handlers: Vec<Box<dyn for<'a> Fn(&'a T)>>,
}

But, those that responded have a better solution

@nologik Unfortunately in this case, the for<'a> syntax doesn't help, because the problematic lifetime is the one on the type used in place of T (i.e. the Message struct), not the one on the reference.

But in my playground link, Signal<Message<'a, T>> is still annotated with the 'a lifetime. The only difference is the variance!

Because fn(&T) is contravariant in T, you can pass the shorter-lived &data to the longer-lived signal, because you can coerce Signal<Message<'a, T>> to Signal<Message<'b, T>> for any 'a: 'b.

This doesn't work when Signal<T> is covariant or invariant in T, as in the original code.

It's natural to expect dyn Fn(&T) to have the same subtyping as fn(&T). The only cases where this would not work involve UnsafeCell. Unfortunately, the compiler needs to care about these cases.

Where would the cast of signal's type happen? It can't happen when passing it to emit because it takes an &mut Signal<Message<'a, i32>>, and the mutable reference makes it invariant.

See also this example where Signal is invariant, but it still compiles because (I claim) the new type has a simpler destructor.

Aha, that makes sense. Thanks for the correction. So fn(&T) works because it has no destructor, not because of its variance.

Yes, and the destructor on Vec doesn't cause problems because it is annotated with #[may_dangle].

@whatisaphone Sorry for hijacking your thread for discussing why it compiles in a weird edge case. You don't need to understand all of this. The answer to your question is that the compiler is worried that your signal variable contains a reference to the data variable, so it wants to make sure that signal is destroyed before data is. You will probably have to restructure your code to avoid this issue, since I assume the data can't outlive the signal struct in practice.