What's the lifetime of the output of as_deref_mut of Option<Box<dyn Trait>>

Trying to get an option mut dyn trait object from an option box, the following two C construct codes are not having the same behavior.

trait X {
    fn x(&mut self);
}

struct XX;

impl X for XX {
    fn x(&mut self) {
        todo!()
    }
}

struct C<'a> {
    mut_ref: Option<&'a mut dyn X>,
}

fn main() {
    let mut b: Option<Box<dyn X>> = Some(Box::new(XX));

    // error[E0597]: `b` does not live long enough
    // let c = C {
    //     mut_ref: b.as_deref_mut(),
    // };

    let c = match b.as_deref_mut() {
        None => C { mut_ref: None },
        Some(x) => C { mut_ref: Some(x) },
    };
}

why does b.as_deref_mut() is not able to assign into mut_ref field, while match pattern works?
I think the lifetime of b.as_deref_mut() equals to b, which is the 'a of struct C<'a>, is that right?

1 Like

You are running into trait object lifetimes.

  • Technically, dyn X is not a full type. It has a lifetime parameter, the general form is looking more like dyn X + 'a
  • The type Option<&'a mut dyn X>, is shorthand for Option<&'a mut (dyn X + 'a)>,; the lifetime comes from the &'a mut … reference around it. The more general rules are documented here but the most important cases include &'a dyn Trait and &'a mut dyn Trait using dyn Trait + 'a, and Box<dyn Trait> using dyn Trait + 'static.

Your type annotation let mut b: Option<Box<dyn X>> does not quite mean Box<dyn X + 'static>, because the elided lifetime only follows abovementioned rules in function signature, type aliases, field types, etc… but not on a let in function bodies - in this case, the lifetime is inferred.

For the sake of simplicity let’s first look at the example of having + 'static, anyways

fn main() {
    let mut b: Option<Box<dyn X + 'static>> = Some(Box::new(XX));

    // error[E0597]: `b` does not live long enough
    // let c = C {
    //     mut_ref: b.as_deref_mut(),
    // };

    let c = match b.as_deref_mut() {
        None => C { mut_ref: None },
        Some(x) => C { mut_ref: Some(x) },
    };
}

The function as_deref_mut takes &'a mut Option<Box<T>> and returns Option<&'a T>. With T == dyn X + 'static, this produces Option<&'a mut (dyn X + 'static)> and not the expected Option<&'a mut (dyn X + 'a)>.

The explicit match gets hold of a value x: &'a mut Box<dyn X + 'static>. This coerces to &'a mut dyn X + 'a implicitly. This coercion is supported as a combination of two implicit coercions:

  • implicit deref coercion to &'a mut (dyn X + 'static)
  • unsizing coercion from &'a mut (dyn X + 'static) to &'a mut (dyn X + 'a)

This second part is crucial. The type &'a mut (dyn X + 'lifetime) is invariant in (dyn X + 'lifetime) and thus cannot transform 'lifetime via subtyping.

There is a mechanism to coerce &'a mut (dyn X + 'lifetime) to a shorter 'lifetime anyways, but it is powered by the compiler's reasoning about subtyping coercion. This only works on pointer types or pointer types wrapped in types (such as e.g. Pin) that implement CoerceUnsized based on contained types’ CoercedUnsized implementations.

The TL;DR is: This coercion does not work inside of an Option.

Thus, neither before nor after the call to as_deref_mut can the lifetime be shortened: Both &'a mut Option<Box<dyn X + 'lifetime>> and Option<&'a mut (dyn X + 'lifetime)> cannot be coerced in a way that shortens 'lifetime, the former because it’s double indirection, the latter because the pointer type is in an Option.


Let’s explore the full subtleties of your example now.

Notably, we did not write 'static. Why cannot let mut b: Option<Box<dyn X>> be inferred as let mut b: Option<Box<dyn X + 'a>> with the right lifetime 'a from the beginning? Well it does! This explains the error message’s note tjat

29 | }
   | -
   | |
   | `b` dropped here while still borrowed
   | borrow might be used here, when `b` is dropped and runs the destructor for type `Option<Box<dyn X>>`

The type Option<Box<dyn X + 'a>> not contains a lifetime 'a that’s coming from a borrow of the whole Option<Box<dyn X + 'a>> itself. This runs into the drop check: The type Option<Box<dyn X + 'a>> has a destructor and it can include user code (it’s a trait object anyways, executing arbitrary (safe) user code upon destruction) which can have access to borrows of lifetime as short-lifed 'a as promised by the + 'a. The drop checker complains, since this means that the Option<Box<dyn X + 'a>> value might have access to a borrow of itself while it’s already in the process of being destructed (and parts might be dropped already), so the motivation of this rule is to prevent use-after-free.

E.g. if there was no drop code, it would compile

let mut b: ManuallyDrop<Option<Box<dyn X>>> = ManuallyDrop::new(Some(Box::new(XX)));

// no error
let c = C {
    mut_ref: b.as_deref_mut(),
};

Similar to the discussion above with + 'static, it’s thus explicitly the possibility to shorten the lifetime on the trait object while its borrowed, so that the drop checker only has to worry about Option<Box<dyn X + 'some_longer_lifetime> being dropped, where 'some_longer_lifetime is longer than 'a and thus precludes access of the trait object’s drop code to borrows of length 'a such as the borrow of the whole Option<Box<dyn X + 'some_longer_lifetime> itself.

The above discussion about shortening the lifetime also mentioned &mut T’s invariance in the type parameter T. This is different from &T which is covariant in T. Thus with immutable references, the issue doesn’t appear.

struct C<'a> {
    mut_ref: Option<&'a dyn X>,
}

let mut b: Option<Box<dyn X>> = Some(Box::new(XX));

// no error
let c = C {
    mut_ref: b.as_deref(),
};

In this case, the type returned from the as_deref call, Option<&'a (dyn X + 'some_longer_lifetime) is covariant in 'some_longer_lifetime and can thus coerce implicitly to Option<&'a (dyn X + 'a)> after the call to as_deref.

7 Likes

Here's an alternative formulation which I believe is sufficient for most use cases.

A situation where it's not sufficient is probably nuanced enough that the reference is not sufficient either, because the reference is quite inaccurate about the behavior of the default trait object lifetime.

3 Likes

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.