Why Do Associated Functions in `impl dyn Trait` require 'static Lifetime?

I've been trying to research an answer to this question all day and most results end up relating to Box<dyn Trait + 'static>, and I believe I understand why the 'static lifetime is applied by default in that case.

What I don't understand is why the 'static lifetime is being applied in the following case, which seems unnecessarily restrictive to me:

trait Animal {
    fn name(&self) -> &'static str;
    fn species(&self) -> &'static str;
}

impl dyn Animal {
    fn print_info(&self) { // 'static lifetime is required here
        println!("{} is a {}", self.name(), self.species())
    }
}

fn print_info(animal: &dyn Animal) {
    println!("{} is a {}", animal.name(), animal.species())
}

struct Dog {
    name: &'static str,
}

impl Animal for Dog {
    fn name(&self) -> &'static str {
        self.name
    }

    fn species(&self) -> &'static str {
        "Canine"
    }
}

struct Person {
    my_dog: Dog,
}

impl Person {
    fn get_pet(&self) -> &dyn Animal {
        &self.my_dog
    }
}

fn main() {
    let p = Person {
        my_dog: Dog { name: "Rover" },
    };
    /* 1 */
    println!("{} is a {}", p.get_pet().name(), p.get_pet().species());
    /* 2 */
    print_info(p.get_pet());
    /* 3 */
    p.get_pet().print_info();
}

This fails to compile with the following diagnostic:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `p` does not live long enough
  --> src/main.rs:49:5
   |
49 |     p.get_pet().print_info();
   |     ^----------
   |     |
   |     borrowed value does not live long enough
   |     argument requires that `p` is borrowed for `'static`
50 | }
   | - `p` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

My expectation is that cases 1, 2, and 3 (at the bottom of the code sample) are all equivalent. get_pet() returns a &dyn Animal that case 1 uses directly, case 2 passes to a function, and case 3 invokes an associated function.

Cases 2 and 3, seem virtually identical to me--only the ergonomics of calling the function change. Clearly there's an implication of impl dyn Trait that I am missing. I would super appreciate it if someone could help me understand what's going on under the hood that make cases 2 and 3 different and if there's a way I can use case 3. Thanks so much!

Edit: Link to the above code in Rust Playgroud.

Fixed: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5b38fba057b042fcbbb57d7a4bdc1d9e

-impl dyn Animal {
+impl dyn Animal + '_ {
     fn print_info(&self) {
         println!("{} is a {}", self.name(), self.species())
     }
 }

(Thanks for making a playground!)

2 Likes

dyn Trait always has a lifetime of applicability. It has its own elision rules and sometimes those backfire. Without double-checking myself, the rules are

  • &dyn Trait :arrow_right: &'a dyn Trait + 'a
  • Box<dyn Trait> :arrow_right: Box<dyn Trait + 'static>
  • Bare dyn Trait in e.g. an impl :arrow_right: dyn Trait + 'static
  • dyn Trait + '_ :arrow_right: use the normal elision rules

With that context, we have

impl dyn Animal /* + 'static */ {
    fn print_info(&self) {
        println!("{} is a {}", self.name(), self.species())
    }
}

And also

impl Person {
    //        "Normal" lifetime elision  vvvvvv
    fn get_pet/*<'a>*/(&/*'a*/ self) -> &/*'a*/ dyn Animal /* + 'a */ {
    //                      `&dyn Trait` lifetime elision  ^^^^^^^^^^
        &self.my_dog
    }
}

So when you

//vvvvvvvvv &'tmp dyn Animal + 'tmp
p.get_pet().print_info();
//          ^^^^^^^^^^^ <dyn Animal + 'static>::print_info 
//                      (is the only one implemented)

You're trying to pass a &dyn Animal + 'tmp to something that wants a &dyn Animal + 'static.

@scottmcm's fix changes the implementation to be generic over the lifetime of applicability. (You could also change get_pet instead in this particular case, but changing the implementation is better IMO.)

4 Likes

Thanks for the detailed explanation @quinedot! I didn't realize that a definition like impl dyn Trait {} would imply a lifetime; I had always thought of lifetimes as a thing for references and fat pointer instances. But framing the behavior as due to lifetime elision rules makes sense--dyn Trait, by default, specifies a 'static lifetime. I see how that's reasonable for fat pointer instances, even if it ends up being a little quirky for impls :smiley:.

Thanks for the quick fix @scottmcm! I'm so happy to get rolling again!

1 Like

Yeah, dyn Trait is special in that sense. (You may have erased a type that had a lifetime, or you may have erased a type that has an implementation bound on some lifetime.) Its special elision rules are intended to work 99.9% of the time, but it's probably more like 90%, and thus arguably a net loss.

(One of a few Rust "ergonomic improvements" to "make things simpler" that I feel optimize for writing and actually increase complexity and confusion.)

Incidentally, if you have a struct Foo<'lifetime>, you can leave it off (Foo) and it will act like wildcard elision (Foo<'_>), sort of like dyn Trait (but with less special casing). That one is more widely recognized as a mistake; you can #![deny(elided_lifetimes_in_paths)] if you'd like to protect yourself from it. (It doesn't apply to dyn applicability lifetimes though.)

2 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.