"lifetime may not live long enough" error that occurs only with "dyn Trait"

Hello.

This code compiles:


fn test<'a>(s: Option<&'a mut Box<u32>>) {
    consume(s.map(|x| x.as_mut()));
}

fn consume<'a>(s: Option<&'a mut u32>) {}

But this code doesn’t:


trait Test {}

fn test<'a>(s: Option<&'a mut Box<dyn Test>>) {
    // error: lifetime may not live long enough
    //     |
    // 167 | fn test<'a>(s: Option<&'a mut Box<dyn Test>>) {
    //     |         -- lifetime `'a` defined here
    // 168 |     consume(s.map(|x| x.as_mut()));
    //     |                       ^^^^^^^^^^ returning this value requires that `'a` must outlive `'static`
    consume(s.map(|x| x.as_mut()));
}

fn consume<'a>(s: Option<&'a mut dyn Test>) {}

The only difference is the u32 was replaced with dyn Trait.

Why the code doesn’t compile? Why it matters if it is dyn Trait or u32 ?

Thanks you!

edit2: Ok, so there's a lot of subtle things colliding here.

  1. Box<dyn Test> actually means Box<dyn Test + 'static>. It gets this implicit lifetime bound by default.

  2. &mut dyn Test does not get an implicit 'static bound. It's &'tmp dyn Test + 'tmp.

  3. &mut lifetimes are invariant, meaning they have to match exactly, and can't be made shorter. If you have &'long you can't use it where &'short is required.

  4. Except where reborrowing and coercions can create a new &'short mut loan borrowed from a &'long mut one. Reborrowing is "shallow", and will change only the outer lifetime, but coercions are more powerful and can alter dyn Trait.

So I think it looks like this:

trait Test {}

fn test(mut s: Option<&mut Box<dyn Test + 'static>>) {
    consume(s.map(|x| x.as_mut()));
}

fn consume<'a>(s: Option<&'a mut (dyn Test + 'a)>) {}

It works if you change it to:

fn consume<'a>(s: Option<&'a mut (dyn Test + '_)>) {}
or
fn consume<'a>(s: Option<&'a mut (dyn Test + 'static)>) {}

it also works if you do:

consume(s.map(|x| x.as_mut() as _));
2 Likes

Every trait object type has a lifetime bound on its contents, and that lifetime is inferred when not written. The inference rules specify that:

  • Box<dyn Test> is equal to Box<dyn Test + 'static>
  • &'a mut dyn Test is equal to &'a mut (dyn Test + 'a)

Thus, you get a type mismatch. The straightforward fix is to specify &'a mut (dyn Test + 'static) explicitly in the signature of consume.


This is not actually the whole story; &mut dyn Test + 'static actually will coerce to &mut dyn Test + 'a. However, coercions don’t happen to types inside arbitrary generic types; you can't coerce an Option containing the dyn Test type.[1] But you can fix that by annotating the map closure’s return type, to make the coercion happen at a point where the value (x) is not wrapped in Option:

trait Test {}

fn test<'a>(s: Option<&'a mut Box<dyn Test>>) {
    consume(s.map(|x| -> &'a mut dyn Test { x.as_mut() }));
}

fn consume<'a>(s: Option<&'a mut dyn Test>) {}

Compared to the previous solution, this preserves more flexibility of consume() — you can pass it values that contain borrows whose lifetimes are at least 'a but less than 'static. But, if you have no use for that — if every dyn Test must meet a 'static bound for other reasons — then you might as well restrict it for clarity.


  1. A more common example of this lack of coercion is that if you want to make an Option<Box<dyn Test>>, then you have to make sure that you coerce Box<SomeImplementor> to Box<dyn Test> before putting it in the Option, rather than after. ↩︎

2 Likes

This would make sense based on the rest of Rust, but it’s not actually true for trait object lifetime bounds. They can be coerced shorter; you can coerce &mut dyn Trait + 'long to &mut dyn Trait + 'short. @user1984’s problem is just that the coercion didn’t happen. (This coercion is sound, in case you’re wondering, because since the concrete type of a dyn is specified by its vtable pointer, you can’t ever write a new too-short dyn value through the &mut because that wouldn’t update the vtables of other pointers to the value, so the unsound operation already has to be prohibited for more fundamental reasons.)

Yeah, this case is much subtler than it seemed.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.