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!
dyn Traitalways has a lifetime of applicability. It has its own elision rules and sometimes those backfire. Without double-checking myself, the rules are
&dyn Trait&'a dyn Trait + 'a
Box<dyn Trait>Box<dyn Trait + 'static>
Bare dyn Trait in e.g. an impl dyn Trait + 'static
dyn Trait + '_ 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())
}
}
//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.)
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 .
Thanks for the quick fix @scottmcm! I'm so happy to get rolling again!
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.)