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
-supportingstd
types 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
'static
bound, the trait object lifetime is always'static
- If the trait has only non-
'static
lifetime 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.