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 generally result in 'static
being used for the elided lifetime:
#![allow(unused)]
fn main() {
// The elided lifetime is `'static`
static S: &str = "";
const C: &str = "";
}
In the ancient days,, you
used to have to write out the 'static lifetime. Since Rust 1.17, eliding
the lifetime at the top level of a module has been sugar for 'static.
As a result, elided dyn Trait lifetimes will by default also be 'static:
#![allow(unused)]
fn main() {
trait Trait {}
impl Trait for () {}
// These are `&'static (dyn Trait + Sync + 'static)`.
static S: &(dyn Trait + Sync) = &();
const C: &(dyn Trait + Sync) = &();
}
Unfortunately, what happens in other contexts has not been as consistent. For example, this snippet:
#![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) = &();
}
}
Required an explicit lifetime before Rust 1.64, but generated a new anonymous lifetime after that. Since Rust 1.81 this has fired a deny-by-default future-compatibility lint.
There have been at least a few cases of unintentionally causing elided
lifetimes in const context to be 'static. For example, the following
snippet failed to compile from Rust 1.64 to 1.70
due to the lifetime being desugared as 'static.
#![allow(unused)]
fn main() {
fn foo() -> [(); {
// (This is a `const` context)
let a = 10_usize;
let b: &usize = &a;
*b
}] {
[(); 10]
}
}
In these contexts, eliding all the lifetimes is again usually what you want. So should you run into a lifetime error in this context, the best advice I have is to try eliding everything, and if that fails, try being explicit about everything.