Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

Trait Objects, 'static and impl Trait Lifetimes

Reference for rust-lifetime-annotation-advisor · 305 lines

Programming / Rustv1.0.0Updated OCT 21 20267 references6 scripts

Three places where lifetimes appear without being written: the default lifetime of a trait object, the meaning of T: 'static, and which lifetimes a returned impl Trait captures (which changed in edition 2024).

Contents

Default object lifetime rules

Every trait object type has a lifetime bound: dyn Trait + 'x. When you do not write it, the compiler uses the default object lifetime bound (this is not elision):

WrittenMeansRule
Box<dyn Trait>Box<dyn Trait + 'static>container type has no lifetime bound on its parameter -> 'static
Rc<dyn Trait>, Arc<dyn Trait>, Vec<Box<dyn Trait>>+ 'staticsame
&'a dyn Trait&'a (dyn Trait + 'a)a reference's lifetime becomes the default
&'a mut dyn Trait&'a mut (dyn Trait + 'a)same
Ref<'a, dyn Trait> where struct Ref<'a, T: ?Sized + 'a>dyn Trait + 'athe container's T: 'a bound is used
&dyn Trait in a fn signature&'1 (dyn Trait + '1)elided reference lifetime, then the rule above
Box<dyn Trait + '_> in a fn signatureelided lifetime, per normal elision'_ opts into elision
dyn Trait where trait Trait: 'a'athe trait's own lifetime bound is the default
impl dyn Trait { .. }impl dyn Trait + 'staticsee below

So the most common surprise: a Box<dyn Fn(&str)> field or return type can never hold a closure that borrows anything non-'static.

dyn Trait + 'a in structs and returns

rust
use std::fmt::Display;

pub struct Report<'a> {
    // Each formatter may borrow from the data the report is about.
    columns: Vec<Box<dyn Fn(usize) -> String + 'a>>,
}

impl<'a> Report<'a> {
    pub fn new() -> Self {
        Self { columns: Vec::new() }
    }

    pub fn column(mut self, f: impl Fn(usize) -> String + 'a) -> Self {
        self.columns.push(Box::new(f));
        self
    }

    pub fn row(&self, i: usize) -> Vec<String> {
        self.columns.iter().map(|c| c(i)).collect()
    }
}

// Returning a boxed iterator that borrows an argument: say '_ (or a named lifetime).
pub fn labels<'a, T: Display>(items: &'a [T]) -> Box<dyn Iterator<Item = String> + 'a> {
    Box::new(items.iter().map(|x| x.to_string()))
}

fn main() {
    let names = vec!["ada".to_string(), "grace".to_string()];
    let report = Report::new().column(|i| names[i].to_uppercase()); // borrows `names`
    println!("{:?}", report.row(1));
    println!("{:?}", labels(&[1, 2]).collect::<Vec<_>>());
}

Without + 'a on the field, column(|i| names[i]...) fails with "names does not live long enough ... argument requires that names is borrowed for 'static" (or E0521 in a function taking &[String]).

When the object should be 'static (stored in a global registry, sent to a thread), keep the default and own the captured data (move + String/Arc). static_bounds (LT011) points at Box<dyn ..> fields in structs that already borrow (struct X<'a>), because that combination is usually a mistake.

For errors, Box<dyn Error + Send + Sync> (implicitly 'static) is the conventional type; it requires error values that own their data, which is one more reason error structs should not carry lifetimes.

impl dyn Trait blocks

Inherent methods on a trait object type default to dyn Trait + 'static:

rust
pub trait Shape {
    fn area(&self) -> f64;
}

// impl dyn Shape { fn describe(&self) -> String { .. } }
// calling describe() on a `&dyn Shape` that borrows non-'static data fails
// (older compilers: E0772 "`self` has an anonymous lifetime `'_` but calling `describe` introduces
// an implicit `'static` lifetime requirement").
impl dyn Shape + '_ {
    pub fn describe(&self) -> String {
        format!("shape with area {:.1}", self.area())
    }
}

struct Square<'a>(&'a f64);

impl Shape for Square<'_> {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
}

fn main() {
    let side = 2.0;
    let sq = Square(&side);
    let s: &dyn Shape = &sq;
    println!("{}", s.describe());
}

What T: 'static really means

T: 'static reads "values of type T contain no references shorter than 'static" (they own all their data, or borrow only static data). It says nothing about how long a particular value lives.

TypeT: 'static?Why
String, Vec<u8>, u64, Box<str>yesowns everything
Arc<Mutex<Config>>yesowns (shares) everything
&'static stryesborrows only static data
&'a str (non-static 'a)nocontains a shorter borrow
Vec<&'a str>nocontains a shorter borrow
Cow<'static, str>yes
Box<dyn Fn()>yesdyn Fn() + 'static by default

Common misconceptions:

  1. "T: 'static means the value lives until the program ends." A String satisfies 'static and is dropped at the end of its scope like any other value. The bound only means it could be kept forever.
  2. "T: 'static means T is &'static something." Owned types satisfy it too; that is the normal case.
  3. "To fix E0310 I need Box::leak." You need the value to own its data: convert &str to String, clone into an Arc, or restructure so the borrow does not escape (scoped threads, + 'a objects).
  4. "&'static T and T: 'static are interchangeable." fn f(x: &'static str) refuses a String's contents; fn f<T: AsRef<str> + 'static>(x: T) accepts a String.
  5. "Adding 'static everywhere is harmless." It rejects every borrowed instantiation of a generic and spreads to callers. Ask for it only where data escapes (LT009 flags borrow-only functions).

Box::leak(Box::new(value)) does produce a &'static mut T; reserve it for once-per-process data such as configuration loaded at startup (or use std::sync::OnceLock, 1.70+, or LazyLock, 1.80+).

E0310 and where 'static comes from

The requirement always has a source. Find it in the diagnostic ("...so that the type T will meet its required lifetime bounds", "argument requires that ... is borrowed for 'static") and decide at that point.

Source of 'staticWhy it needs itOptions
std::thread::spawnthread may outlive the callermove owned data / Arc; std::thread::scope (1.63+) to borrow
tokio::spawntask may outlive the callerowned data / Arc; JoinSet still needs 'static; restructure as join! / FuturesUnordered in the same task to borrow
Box<dyn Trait> (default)object bound defaults to 'staticBox<dyn Trait + 'a>
dyn Any, TypeId::of::<T>(), downcast_refAny: 'staticown the data; Any cannot hold borrows
static / OnceLock / lazy_staticlives for the whole programown the data
a trait with trait Plugin: 'staticsupertrait boundremove the bound if not needed
rust
use std::any::Any;
use std::fmt::Debug;

// E0310 without 'static: Box<dyn Any> requires T: 'static.
fn erase<T: Any>(value: T) -> Box<dyn Any> {
    Box::new(value)
}

// Only borrowing T: no 'static needed.
fn show<T: Debug + ?Sized>(value: &T) -> String {
    format!("{value:?}")
}

fn main() {
    let any = erase(String::from("owned"));
    assert!(any.downcast_ref::<String>().is_some());
    let local = String::from("borrowed");
    let words: Vec<&str> = local.split(' ').collect();
    println!("{}", show(&words)); // Vec<&str> is not 'static: fine here
}

T: 'a and 'a: 'b bounds

  • T: 'a - every reference inside T outlives 'a. Required to form &'a T. Since Rust 1.31 (RFC 2093) these bounds are inferred for struct fields, so struct Ref<'a, T> { r: &'a T } needs no T: 'a. You still write them on trait objects (Box<dyn Trait + 'a>), on generic functions that build &'a T from scratch, and on GAT where clauses (where Self: 'a).
  • 'long: 'short - 'long outlives 'short. Needed when a function returns data of one lifetime through a reference of another, or a struct holds two related borrows.
rust
// The cache reference may be shorter than the data it hands out.
struct Cache<'data> {
    items: Vec<&'data str>,
}

fn first<'c, 'data: 'c>(cache: &'c Cache<'data>) -> &'data str {
    cache.items.first().copied().unwrap_or("")
}

('data: 'c is implied here by the type &'c Cache<'data> being well-formed, so it can be omitted; writing it documents the relationship.)

impl Trait in argument position

fn f(x: impl Trait) is sugar for an anonymous generic parameter. Lifetimes inside it follow the normal rules: impl Fn(&str) -> &str is higher-ranked over the reference, and an impl Iterator<Item = &str> argument gets a fresh elided lifetime. The caller cannot name the type with turbofish.

Return-position impl Trait: capture rules

The returned hidden type (say Filter<Iter<'a, u32>, {closure}>) may only mention generic parameters the opaque type captures. Capturing a lifetime means callers must treat the result as borrowing it.

Position / editionType & const paramsLifetime params (incl. elided)
free fn / inherent method, edition 2015-2021all in scope capturedonly those named in the bounds (+ 'a, + '_, + use<'a>)
free fn / inherent method, edition 2024all capturedall in scope captured
async fn (all editions)allall
-> impl Trait in a trait or trait impl (RPITIT, 1.75+)allall
with + use<..> (1.82+)exactly the listed ones (in 1.82 every type param must be listed)exactly the listed ones

Edition 2021 failure (E0700) and its fixes:

rust
// error[E0700] in edition 2021: hidden type captures lifetime that does not appear in bounds
// fn evens(xs: &[u32]) -> impl Iterator<Item = u32> { xs.iter().copied().filter(|x| x % 2 == 0) }

fn evens(xs: &[u32]) -> impl Iterator<Item = u32> + '_ {
    xs.iter().copied().filter(|x| x % 2 == 0)
}

+ 'a is an outlives bound that happens to force capture. With two lifetimes it is wrong: fn f<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = char> + 'a cannot return something that captures 'b. Before use<..> the workaround was the Captures trick:

rust
trait Captures<'a> {}
impl<'a, T: ?Sized> Captures<'a> for T {}

// Pre-1.82, edition 2021: capture both lifetimes without claiming the result outlives either.
fn zip_chars<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = (char, char)> + Captures<'a> + Captures<'b> {
    x.chars().zip(y.chars())
}

With Rust 1.82+ write -> impl Iterator<Item = (char, char)> + use<'a, 'b> instead, or rely on edition 2024.

Precise capturing with use<..> (1.82)

use<..> lists exactly what the opaque type may capture. It works in every edition from Rust 1.82.

rust
// Edition 2024: without use<T>, this would capture the slice lifetime and keep `v` borrowed.
fn indices<T>(xs: &[T]) -> impl Iterator<Item = usize> + use<T> {
    0..xs.len()
}

fn main() {
    let mut v = vec![10, 20, 30];
    let it = indices(&v);
    v.push(40); // allowed: `it` does not borrow `v`
    for i in it {
        println!("{i}");
    }
}

Rules worth knowing:

  • use<> captures nothing; use<'a, T> captures 'a and T.
  • In Rust 1.82 all type and const parameters in scope must appear in the list; only lifetimes can be omitted.
  • Only one use<..> bound is allowed per impl Trait.
  • In trait definitions (RPITIT) all parameters are always captured; use<..> support there arrived after 1.82, so check your toolchain before using it in traits.

Migrating to edition 2024

  • cargo fix --edition runs the impl_trait_overcaptures lint, which adds use<..> where the new rules would capture more than before and change borrow-checking for callers.
  • After migrating, new code can usually drop + '_/Captures workarounds: the default now captures.
  • Add use<..> when callers need to keep using an argument while holding the result (iterators over owned copies, builders that return an independent value).
  • Edition 2024 needs Rust 1.85+ and edition = "2024" in Cargo.toml. lifetime_smells (LT012) lists every -> impl Trait with borrowed inputs and no use<..> when the crate is on 2024.

impl Trait in traits and async fn

  • async fn in traits and -> impl Trait in traits are stable since Rust 1.75. Their returned types capture all generic parameters, including all input lifetimes.
  • Callers cannot add Send to the returned future of a trait's async fn. If implementors are used with tokio::spawn, declare the method as fn get(&self) -> impl Future<Output = T> + Send; (implementors can still write async fn) or use the trait-variant crate to generate a Send variant.
  • async fn in traits is not dyn-compatible (object safe); for dyn Trait use `Pin<Box<dyn Future + Send
    • '_>>returns (theasync-traitcrate generates exactly that, with'async_trait` lifetimes).
More in Rust

rust-borrow-checker-error-explainer

Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.

rust-cargo-workspace-designer

Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.

rust-error-handling-thiserror-anyhow-designer

Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.

rust-tokio-async-service-builder

Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.

rust-trait-design-reviewer

Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.

rust-unsafe-block-auditor

Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.