Basic guidelines and subtleties
As a reminder, dyn Trait is a type constructor which is parameterized with a
lifetime; a fully resolved type includes the lifetime, such as dyn Trait + 'static.
The lifetime can be elided in many situations, in which case the actual lifetime
used may take on some default lifetime, or may be inferred.
When talking about default trait object (dyn Trait) lifetimes, we're talking about
situations where the lifetime has been completely elided. If the wildcard lifetime
is used (dyn Trait + '_), then the normal lifetime elision rules
usually apply instead. (The exceptions are rare, and you can usually be explicit
instead if you need to.)
For a completely elided dyn Trait lifetime, you can start with these
general guidelines for traits with no lifetime bounds (which are the vast majority):
- In function bodies, the trait object lifetime is inferred (i.e. ignore the following bullets)
- For references like
&'a dyn Trait, the default is the same as the reference lifetime ('a) - For
dyn-supportingstdtypes with lifetime parameters such asRef<'a, T>, it is also'a - For non-lifetime-parameter types like
Box<dyn Trait>, and for baredyn Trait, it's'static
And for the (rare) trait with lifetime bounds:
- If the trait has a
'staticbound, the trait object lifetime is always'static - If the trait has only non-
'staticlifetime bounds, you're better off being explicit
This is a close enough approximation to let you understand dyn Trait
lifetime elision most of the time, but there are exceptions to these
guidelines (which are explored on the next couple of pages).
There are also a few subtleties worth pointing out within these guidelines, which are covered immediately below.
Default 'static bound gotchas
The most likely scenario to run into an error about dyn Trait lifetime is
when Box or similar is involved, resulting an implicit 'static constraint.
Those errors can often be addressed by either adding an explicit 'static
bound, or by overriding the implicit 'static lifetime. In particular, using
'_ will usually result in the "normal" (non-dyn Trait) lifetime elision for
the given context.
#![allow(unused)] fn main() { trait Trait {} impl<T: Trait> Trait for &T {} // Remove `+ 'static` to see an error fn with_explicit_bound<'a, T: Trait + 'static> (t: T) -> Box<dyn Trait> { Box::new(t) } // Remove `+ 'a` (in either position) to see an error fn with_nonstatic_box<'a, T: Trait + 'a>(t: T) -> Box<dyn Trait + 'a> { Box::new(t) } // Remove `+ '_` to see an error fn with_fn_lifetime_elision(t: &impl Trait) -> Box<dyn Trait + '_> { Box::new(t) } }
This can be particularly confusing within a function body, where a
Box<dyn Trait> variable annotation acts differently from a Box<dyn Trait>
function input parameter annotation:
#![allow(unused)] fn main() { trait Trait {} impl Trait for &i32 {} // In this context, the elided lifetime is `'static` fn requires_static(_: Box<dyn Trait>) {} fn example() { let local = 0; // In this context, the annotation means `Box<dyn Trait + '_>`! // That is why it can compile on it's own, with the local reference. let bx: Box<dyn Trait> = Box::new(&local); // So despite using the same syntax, this call cannot compile. // Uncomment it to see the compilation error. // requires_static(bx); } }
impl headers
The dyn Trait lifetime elision applies in impl headers, which can lead to
implementations being less general than possible or desired:
#![allow(unused)] fn main() { trait Trait {} trait Two {} impl Two for Box<dyn Trait> {} impl Two for &dyn Trait {} }
Two is implemented for
Box<dyn Trait + 'static>&'a (dyn Trait + 'a)for any'a(the lifetimes must match)
Consider using implementations like the following if possible, as they are more general:
#![allow(unused)] fn main() { trait Trait {} trait Two {} // Implemented for all lifetimes impl Two for Box<dyn Trait + '_> {} // Implemented for all lifetimes such that the inner lifetime is // at least as long as the outer lifetime impl Two for &(dyn Trait + '_) {} }
Alias gotchas
Similar to impl headers, elision will apply when defining a type alias:
#![allow(unused)] fn main() { trait MyTraitICouldNotThinkOfAShortNameFor {} // This is an alias to `dyn ... + 'static`! type MyDyn = dyn MyTraitICouldNotThinkOfAShortNameFor; // The default does not "override" the type alias and thus // requires the trait object lifetime to be `'static` fn foo(_: &MyDyn) {} // As per the `dyn` elision rules, this requires the trait // object lifetime to be the same as the reference... fn bar(d: &dyn MyTraitICouldNotThinkOfAShortNameFor) { // ...and thus this fails as the lifetime cannot be extended foo(d); } }
More generally, elision does not "penetrate" or alter type aliases.
This includes the Self alias within implementation blocks.
#![allow(unused)] fn main() { trait Trait {} impl dyn Trait { // Error: requires `T: 'static` fn f<T: Trait>(t: &T) -> &Self { t } } impl<'a> dyn Trait + 'a { // Error: requires `T: 'a` fn g<T: Trait>(t: &T) -> &Self { t } } }
See also how type aliases with parameters behave.
'static traits
When the trait itself is 'static, the trait object lifetime has an implied
'static bound. Therefore if you name the trait object lifetime explicitly,
the name you give it will also have an implied 'static bound. So here:
use core::any::Any; // n.b. trait `Any` has a `'static` bound fn example<'a>(_: &'a (dyn Any + 'a)) {} fn main() { let local = (); example(&local); }
We get an error that the borrow of local must be 'static. The problem is
that 'a in example has inherited the 'static bound ('a: 'static), and
we also gave the outer reference the lifetime of 'a. This is a case where
we don't actually want them to be the same.
The most ergonomic solution is to always completely elide the trait object
lifetime when the trait itself has a 'static bound. Unlike other cases,
the trait object lifetime is independent of the outer reference lifetime when
the trait itself has a 'static bound, so this compiles:
use core::any::Any; // This is `&'a (dyn Any + 'static)` and `'a` doesn't have to be `'static` fn example(_: &dyn Any) {} fn main() { let local = (); example(&local); }
Any is the most common trait with a 'static bound, i.e. the most likely
reason for you to encounter this scenario.
static contexts
In some contexts like when declaring a static, it's possible to elide the
lifetime of types like references; doing so will result in 'static being
used for the elided lifetime:
#![allow(unused)] fn main() { // The elided lifetime is `'static` static S: &str = ""; const C: &str = ""; }
As a result, elided dyn Trait lifetimes will by default also be 'static,
matching the inferred lifetime of the reference. In contrast, this fails
due to the outer lifetime being 'static:
#![allow(unused)] fn main() { trait Trait {} impl Trait for () {} struct S<'a: 'b, 'b>(&'b &'a str); impl<'a: 'b, 'b> S<'a, 'b> { const T: &(dyn Trait + 'a) = &(); } }
In this context, eliding all the lifetimes is again usually what you want.