A trait object is a value used through a trait, written dyn Trait, where
the concrete type is only known at run time. Box<dyn Error> is a box holding
some value that implements Error, and &dyn Debug is a reference to some
value that implements Debug.
Every trait object type also has a lifetime bound, written after a +:
dyn Debug + 'a means "some Debug value that is valid for at least 'a".
The bound limits which borrowed data the hidden value may contain. If the value
holds a &'x str, then 'x must outlive the object's bound.
You rarely write that bound, so the compiler fills in a default. Consider:
Box<dyn Error>
In a function signature, struct field or type alias, that type means
Box<dyn Error + 'static>. The 'static bound says the boxed error may not
borrow anything that could be freed earlier. That default explains errors that
mention 'static in code where you never wrote 'static.
The default rules
The Rust Reference calls these rules default trait object lifetime bounds.
For traits that declare no lifetime bound of their own (Error, Debug, Fn
and most traits you write), they reduce to this:
- The containing type decides first. If the trait object is a type
argument of a type whose parameter has exactly one lifetime bound, that bound
is used.
&'a TrequiresT: 'a, so&'a dyn Foogets'a. - If the containing type's parameter has two or more lifetime bounds, there
is no default and you must write the bound. The compiler reports
error[E0228]: cannot deduce the lifetime bound for this trait object type from context. - If the containing type gives no bound, as with
Box<T>,Vec<T>,Rc<T>andArc<T>, the bound is'staticin signatures, struct fields and type aliases, and is inferred inside function bodies (see below).
When the trait itself declares a lifetime bound, for example trait Bar<'a>: 'a,
extra rules apply. The Reference lists them; this article only uses traits
without such bounds.
The two cases you will see most often:
Box<dyn Foo> // means Box<dyn Foo + 'static>
&'a dyn Foo // means &'a (dyn Foo + 'a)
To ask for something other than the default, write the bound yourself. In a
function's return type you can write '_, which takes the lifetime chosen by
the ordinary lifetime elision rules (for a function with one reference
parameter, that parameter's lifetime):
Box<dyn Foo + '_>
'_ is not allowed in a struct field. There the compiler reports
error[E0106]: missing lifetime specifier, and you need a named lifetime
parameter on the struct instead.
Where it shows up
Returning a boxed value built from a reference:
use std::fmt::Debug;
fn wrap(s: &str) -> Box<dyn Debug> {
Box::new(s)
}
error: lifetime may not live long enough
returning this value requires that `'1` must outlive `'static`
Here '1 is the compiler's name for the unwritten lifetime of s. The box
holds that &str, but the return type, by the default, promises a
dyn Debug + 'static. The compiler's help text suggests the fix: let the object
carry the parameter's lifetime.
fn wrap(s: &str) -> Box<dyn Debug + '_> {
Box::new(s)
}
A struct field holding boxed closures:
struct Registry { handlers: Vec<Box<dyn Fn(i64) -> i64>> }
The field type means Vec<Box<dyn Fn(i64) -> i64 + 'static>>. The declaration
itself compiles without a warning. The error appears where you store a closure
that captures a reference:
let t = vec![1i64];
let tr: &Vec<i64> = &t;
let mut r = Registry { handlers: Vec::new() };
r.handlers.push(Box::new(move |x| x + tr[0]));
// error[E0597]: `t` does not live long enough
The move closure captures tr, a borrow of t, so the closure is only valid
while t is. Storing it in the field requires the borrow to last for
'static, which a local variable cannot provide. The error is reported at the
borrow of t, not at the struct, but current compilers add a note that points
at the cause:
note: due to object lifetime defaults, `Box<dyn Fn(i64) -> i64>` actually means `Box<(dyn Fn(i64) -> i64 + 'static)>`
If the registry really should hold borrowing closures, give the struct a lifetime parameter and use it as the bound:
struct Registry<'a> { handlers: Vec<Box<dyn Fn(i64) -> i64 + 'a>> }
With that declaration the same four lines compile, and the borrow checker then
ensures r is not used after t is dropped.
Check your understanding
Why does &'a Box<dyn Foo> still contain a 'static object, when &'a dyn Foo does not?
Show answer
Hide answer
Because the type that directly contains the trait object sets the default, and here that is the Box, not the reference.
The trait object is the type argument of Box<...>. Box<T> puts no lifetime bound on T, so the object becomes dyn Foo + 'static. The resulting Box<dyn Foo + 'static> is then the type behind &'a, and the reference's 'a does not reach inside it.
So &'a Box<dyn Foo> means &'a Box<dyn Foo + 'static>, while &'a dyn Foo means &'a (dyn Foo + 'a). A function declared to return &'a Box<dyn Foo> cannot return a &'a Box<dyn Foo + 'a>.
Separately, Clippy's borrowed_box lint, which warns by default, flags a &Box<T> parameter and suggests &T. Under a configuration that turns warnings into errors (cargo clippy -- -D warnings), that warning fails the check. Taking &dyn Foo avoids the lint and gives the 'a default.
Inside function bodies the bound is inferred
In a let type annotation inside a function body, an omitted object lifetime
is inferred from how the value is used rather than fixed at 'static:
use std::fmt::Debug;
fn show() {
let s = String::from("hi");
let b: Box<dyn Debug> = Box::new(&s); // compiles
println!("{b:?}");
}
The 'static default applies to types written outside expressions: function
signatures, struct and enum fields, and type aliases. If the annotation above
used type DebugBox = Box<dyn Debug>; instead, the alias would carry
'static and the same line would fail with error[E0597]: s does not live long enough.
So a let that compiles is not evidence that the same type works in a
signature or a field. Check those positions separately.
E0759 is no longer emitted
Searching for this error may lead you to E0759, whose explanation covers
this case: why fn bar(x: &i32) -> Box<dyn Debug> fails and why
Box<dyn Debug + '_> fixes it. Current compilers no longer use that code:
$ rustc --explain E0759
#### Note: this error code is no longer emitted by the compiler.
The Box<dyn Debug> part of the explanation still applies, but in your
terminal you will see the uncoded error: lifetime may not live long enough
shown above. Its impl Debug example is out of date: since the 2024 edition,
a return-position impl Trait type may use the lifetimes of the function's
parameters, so fn foo(x: &i32) -> impl Debug { x } compiles without + '_.
The 'static default described in this article applies to dyn Trait, not to
impl Trait.
Check your understanding
Box<dyn Error> and Box<dyn Error + Send + Sync + 'static> both appear in real code. What is the difference?
Show answer
Hide answer
In terms of the object lifetime, there is none. In a signature, Box<dyn Error> already means Box<dyn Error + 'static>, so writing 'static explicitly only makes the default visible to the reader.
The difference is Send + Sync. A Box<dyn Error> may hold an error type that cannot be moved to or shared with another thread, so the box itself is neither Send nor Sync. For example, a std::thread::spawn closure that returns Result<(), Box<dyn Error>> is rejected with error[E0277]: dyn std::error::Error cannot be sent between threads safely, because spawn requires its result to be Send. Returning Result<(), Box<dyn Error + Send + Sync>> compiles.
Both forms work with the ? operator for standard error types such as ParseIntError. Use the Send + Sync form when the error may need to cross a thread boundary, for example in a library whose callers might use threads. The plain form is enough when the error stays on one thread, as in many main functions.
Summary
| you write | in a signature, field or alias it means | why |
|---|---|---|
Box<dyn Foo> |
Box<dyn Foo + 'static> |
Box<T> puts no lifetime bound on T |
&'a dyn Foo |
&'a (dyn Foo + 'a) |
&'a T requires T: 'a |
&'a Box<dyn Foo> |
&'a Box<dyn Foo + 'static> |
the Box directly contains the object |
Box<dyn Foo + '_> (return type) |
the lifetime chosen by elision | explicit bound replaces the default |
When an error mentions 'static and your code never wrote it, one place to
check is any dyn Trait type without an explicit + 'lifetime bound.