Understanding lifetimes of Box<dyn SomeTrait>

I have a trait, Out<T> below, that I implement for a variety of types, including slice references.
I've aliased boxed dyn trait objects, and I'm curious about the lifetime specification.

My trait impl for the slice ref doesn't require explicit lifetime declarations, I assume that the compiler and lifetime checker see that the reference only need to exist for the length of the function call. But when I make a trait object I need to specify a lifetime, here I use 'static. Can someone explain the lifetime in this case? I realize that the type needs to life as long as the box, which can life as long as the program. But the call to the trait method, send, does this lifetime need to bubble up into the owning struct?

I see that if I replace 'static with 'a and then add <'a> to my struct and impl declarations this still compiles: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f596f9634ee22b96d8da1928e4b15878

I guess I'm just trying to get a better understanding of the meaning of
dyn Out<&'static [i64]> vs dyn Out<&'a [i64]>

pub trait Out<T> {
    fn send(&self, value: T);
}

pub struct O;
type Outi64 = Box<dyn Out<i64>>;
type Outi64S = Box<dyn Out<&'static [i64]>>;

impl Out<i64> for O {
    fn send(&self, _value: i64) {
        // impl
    }
}

impl Out<&[i64]> for O {
    fn send(&self, _value: &[i64]) {
        // impl
    }
}

pub struct A {
    o1: Outi64S,
    o2: Outi64,
}

impl A {
    pub fn new() -> Self {
        Self {
            o1: Box::new(O),
            o2: Box::new(O),
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
warning: field is never read: `o1`
  --> src/lib.rs:22:5
   |
22 |     o1: Outi64S,
   |     ^^^^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: field is never read: `o2`
  --> src/lib.rs:23:5
   |
23 |     o2: Outi64,
   |     ^^^^^^^^^^

warning: 2 warnings emitted

    Finished dev [unoptimized + debuginfo] target(s) in 1.32s

Since your trait Out<T> is very similar to Fn(T), let’s try out dyn Fn(...), too.

So you could define these

type Fni64 = Box<dyn Fn(i64)>;
type Fni64SStatic= Box<dyn Fn(&'static [i64])>;

just like you did for your Outi64 and Outi64S.

Let’s use them:

pub struct A {
    o1: Fni64SStatic,
    o2: Fni64,
}

impl A {
    pub fn new() -> Self {
        Self {
            o1: Box::new(|_value| ()),
            o2: Box::new(|_value| ()),
        }
    }
}

Alright, this seems to work.


With Fn traits you will however notice (if you try it) that the following works, too:

type Fni64SFor = Box<dyn Fn(&[i64])>;

and its usage

pub struct B {
    o1: Fni64SFor,
    o2: Fni64SStatic,
}

impl B {
    pub fn new() -> Self {
        Self {
            o1: Box::new(|_value| ()),
            o2: Box::new(|_value| ()),
        }
    }
}

Let’s go further and apply the fields of B:

fn foo(a: &B) {
    let x: [i64; 1] = [1];
    static Y: [i64; 1] = [2];
    
    (a.o1)(&Y);
    (a.o2)(&Y);
    (a.o1)(&x);
    // (a.o2)(&x); // x does not live long enough
}

Not surprisingly, the 'static lifetime introduces some unfortunate restrictions, but how does this other unspecified lifetime magically work?

The answer is: higher rank trait bounds. (Also see in the nomicon.)

Writing Fn(&[i64]) has some special lifetime elision rules that make this translate to for<'a> Fn(&'a [i64]). More generally, the ordinary function elision rules, in a way, apply here too, like Fn(&[i64]) -> &[i64] becoming for<'a> Fn(&'a [i64]) -> &'a [i64] or Fn(&[i64], &[i64]) becoming for<'a, 'b> Fn(&'a [i64], &'b [i64]). Note that all of this always applies, not only in the context of dyn.

So knowing about this kind of translation presents us with another option for your Out type that you didn’t consider yet, namely:

type Outi64SFor = Box<dyn for<'a> Out<&'a [i64]>>;

Now, an O with this impl

impl Out<&[i64]> for O {
    fn send(&self, _value: &[i64]) {
        // impl
    }
}

will be coercible into a dyn for<'a> Out<&'a [i64]>. The for<'a> ... syntax means: “The ... trait is implemented for all possible lifetimes 'a ”. And the impl, using lifetime elision itself, is in effect generic like this:

impl<'a> Out<&'a [i64]> for O {
    fn send(&self, _value: &[i64]) {
        // impl
    }
}

so there is an impl for every 'a.


Note that regarding lifetimes and dyn, there’s a different question to be had: What if you have an

impl<'a> SomeTrait for &'a i64

will &'a i64 be coercible to dyn SomeTrait?

And the answer is: when 'a is not 'static, in most cases: NO. Because in most cases dyn SomeTrait actually stands for dyn SomeTrait + 'static (read: dyn (SomeTrait + 'static)), and &'a i64: 'static is not satisfied. This is another form of lifetime elision, you can find out more in the reference on this topic as well.


Edit: I guess I haven’t really answered your question yet.

The difference is noticeable when you try to call .o1.send(&x) on some x. The 'static version will only accept x that can be borrowed for a static lifetime, which is mostly just static variables and stuff you Box::leaked. The version with 'a can accept shorter-lived x, but all of them must have the same lifetime (if you call send multiple times). The for<'a> ... version I presented is the most flexible.


Edit2: Some demonstration of those differences of how .send can be used: (playground)

3 Likes

ooh, thanks so much @steffahn this is very helpful and informative! The above for is exactly what I'm looking for!!

1 Like

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.