Why lifetime elision doesn't apply here?

Hi,

Could you help me understanding why I need all this lifetime bound for this code:

struct Foo {
    vec: Vec<i32>,
}

impl Foo {
    fn ints<'a>(&'a self) -> Box<dyn Iterator<Item = &'a i32> + 'a> {
        Box::new(self.vec.iter())
    }
}

I was thinking that lifetime elision would kick in and applies same self lifetime for all stuff.

struct Foo {
    vec: Vec<i32>,
}

impl Foo {
    fn ints(&self) -> Box<dyn Iterator<Item = &i32> + '_> {
        Box::new(self.vec.iter())
    }
}

works, here. The issue is that implicit elision of trait objects has priority over implicit elision in function signatures, so the rules for the former are the one that apply. And an implicitly elided lifetime in a boxed trait object (with no other lifetimes around the trait) defaults to 'static. By using + '_ to explicitly elide the lifetime, you opt-out of trait object lifetime elision rules (which, by the way, can be quite surprising, so I find it best not to rely on them too much), and at that point classic function-signature-lifetime-elision rules can apply :slightly_smiling_face:

12 Likes

Just curious: Do you have a (simple-ish) example where you find them surprising?

I'd say that something along the following lines is the most classical example.

We start off a trait we'd like to make "dyn-friendly":

trait Spawner {
    fn spawn(&self, f: impl 'static + Send + FnOnce());
}

It can't be used with dyn / is not "dyn-friendly" / is not dyn-safe / is not object safe because it features a generic method (fn spawn<F: …FnOnce()>(&self, f: F)). The workaround for this, when applicable, is to replace the generic-based polymorphism for the argument with dyn-based polymorphism:

fn spawn_non_generic(&self, f: Box<dyn Send + FnOnce()>)

and then you can still feature the original method as a frontend, provided it's explicitly made unavailable to dyn Spawn by using a where Self: Sized bound:

trait Spawner {
    fn spawn(&self, f: impl 'static + Send + FnOnce())
    where
        Self: Sized,
    {
        /* default implementation; can be overridden to be made more efficient */
        self.spawn_non_generic(Box::new(f));
    }

    fn spawn_non_generic(&self, f: Box<dyn Send + FnOnce()>);
}

And at this point you can even make the trait object version feature a similar frontend!

impl dyn Spawner {
    fn dyn_spawn(&self, f: impl 'static + Send + FnOnce()) {
        /* Only possible implementation for the trait object */
        self.spawn_non_generic(Box::new(f))
    }
}

and you can even then go, and test your stuff:

struct MySpawner;
impl Spawner for MySpawner {
    fn spawn_non_generic(&self, f: Box<dyn Send + FnOnce()>) {
        let _ = ::std::thread::spawn(f);
    }
}

let s = Box::new(MySpawner) as Box<dyn Spawner>;
s.dyn_spawn(move || { // <- OK
    println!("Hello, World!");
});

Until now everything is great, except when you find out that the following fails:

fn _fails(s: &dyn Spawner) {
    s.dyn_spawn(move || {
        println!("Hello, World!");
    });
}
  • Playground

  • Minimal repro:

    trait Spawner {
        fn spawn_non_generic(&self, f: Box<dyn Send + FnOnce()>);
    }
    
    impl dyn Spawner {
        fn dyn_spawn(&self, f: impl 'static + Send + FnOnce()) {
            self.spawn_non_generic(Box::new(f))
        }
    }
    
    fn _fails(s: &dyn Spawner) {
        s.dyn_spawn(|| {});
    }
    
Why it fails

impl dyn Spawner { stands for:

impl (dyn 'static + Spawner) {

but &'lt dyn Spawner stands for &'lt (dyn Spawner + 'lt) (unless Spawner : 'static) so that we end up with:

impl (dyn 'static + Spawner) { 
    fn dyn_spawn(&self…
}
                          // expected 'static
                          //   /----<------------<--+
                          // vvv                    ^
fn _fails<'lt> (s: &'lt (dyn 'lt + Spawner)) {   // |
    s.dyn_spawn… // |---->--------------->----------+ 
}

Finally, we also have fun{n,k}y situations such as:

fn _ok(_: Box<dyn Send + 'static>) -> &str {
    "hi"
}

type BoxedDynSend = Box<dyn Send + 'static>;

fn _fails(_: BoxedDynSend) -> &str {
    "hi"
}

Although that's mostly a(nother) misinteraction with lifetime elision for function signatures…

2 Likes

Thanks for the examples!

This syntax is unknown to me:

impl dyn Spawner {

Edit: After looking up this previously unknown-to-me syntax I get what it does now. Thanks.

2 Likes

A lot of things made a lot more sense to me once it clicked that dyn Trait is a concrete, statically-known type. Although you have to deal with its elision-happy lifetime bound and being non-Sized, it's still a type, and there's no reason you can't impl it, etc etc.

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.