Outlives bounds, variance, PhantomData, higher-ranked trait bounds, generic associated types, iterators that
hand out references, closures, and the places where the borrow checker is stricter than necessary. Every
block compiles on stable Rust 2021 unless it says otherwise.
Contents
- Outlives bounds:
'a: 'bandT: 'a - Variance
PhantomData- Higher-ranked trait bounds (HRTB)
- Iterators over borrowed data
- Lending iterators with GATs
- Closures that take or return references
- When the checker is too strict
Outlives bounds: 'a: 'b and T: 'a
'a: 'b reads "'a outlives 'b": data valid for 'a is valid for at least 'b. You rarely need it,
because covariance already shortens lifetimes for you. It appears when a function must return data of the
longer lifetime through a structure of the shorter one.
struct Context<'cfg> { prefix: &'cfg str }
// The returned &str lives as long as the config ('cfg), not as long as the short borrow of the context ('c).
fn prefix<'c, 'cfg: 'c>(ctx: &'c Context<'cfg>) -> &'cfg str {
ctx.prefix
}T: 'a means "every reference inside T lives at least 'a". Since Rust 2018 the compiler infers these
bounds on struct fields (struct Ref<'a, T> { r: &'a T } no longer needs where T: 'a), but you still write
them on functions and impls that place a T behind &'a:
fn store<'a, T: 'a>(slot: &mut Vec<Box<dyn std::fmt::Debug + 'a>>, v: T)
where
T: std::fmt::Debug,
{
slot.push(Box::new(v)); // without T: 'a -> E0310 "the parameter type `T` may not live long enough"
}Variance
Variance decides whether Type<'long> may be used where Type<'short> is expected.
| Type | In 'a | In T | Consequence |
|---|---|---|---|
&'a T | covariant | covariant | a longer borrow can always be used as a shorter one |
&'a mut T | covariant | invariant | &mut Vec<&'static str> cannot be passed as &mut Vec<&'a str> |
Box<T>, Vec<T>, Option<T> | - | covariant | behave like T |
Cell<T>, RefCell<T>, UnsafeCell<T> | - | invariant | interior mutability = mutation through & |
fn(T) -> U | - | contravariant in T, covariant in U | |
*const T / *mut T | - | covariant / invariant | |
PhantomData<X> | as X | as X | used to choose variance |
Invariance is why &'a mut X<'a> hurts (LT007): the inner 'a cannot shrink, so the outer mutable borrow
must last for all of 'a.
fn push_local<'a>(v: &mut Vec<&'a str>, s: &'a str) {
v.push(s);
}
fn demo() {
let mut names: Vec<&'static str> = vec!["a"];
let owned = String::from("b");
// push_local(&mut names, &owned); // error: `owned` does not live long enough
// // (&mut is invariant: 'a is forced to 'static)
let mut names2: Vec<&str> = names.clone(); // a Vec with a shorter element lifetime
push_local(&mut names2, &owned);
names.push("c");
}PhantomData
Use PhantomData when a type logically holds a lifetime or a T that no field mentions (E0392 otherwise).
Pick the form that gives the right variance and auto traits:
| Marker | Means | Variance | Send/Sync like |
|---|---|---|---|
PhantomData<&'a T> | borrows a T for 'a | covariant in 'a, T | &'a T |
PhantomData<&'a mut T> | mutably borrows | covariant 'a, invariant T | &'a mut T |
PhantomData<T> | owns a T (drop check considers it) | covariant | T |
PhantomData<fn() -> T> | produces T, owns none | covariant, no drop-check impact | always Send + Sync |
PhantomData<fn(T)> | consumes T | contravariant | always Send + Sync |
PhantomData<*const T> | opts out of Send/Sync | covariant | neither |
use std::marker::PhantomData;
/// A cursor into a buffer it does not store, tied to the buffer's lifetime.
struct Cursor<'buf> {
ptr: *const u8,
len: usize,
_buf: PhantomData<&'buf [u8]>,
}
impl<'buf> Cursor<'buf> {
fn new(buf: &'buf [u8]) -> Self {
Cursor { ptr: buf.as_ptr(), len: buf.len(), _buf: PhantomData }
}
fn len(&self) -> usize { self.len }
}
/// A typed id that borrows nothing.
struct Id<T> { raw: u32, _t: PhantomData<fn() -> T> }Higher-ranked trait bounds (HRTB)
for<'a> F: Fn(&'a str) -> &'a str means "for every lifetime the caller picks, F works". It is what the
compiler writes for you when you elide lifetimes inside Fn sugar:
// These two bounds are identical:
fn apply<F: Fn(&str) -> &str>(f: F, s: &str) -> String { f(s).to_string() }
fn apply_explicit<F>(f: F, s: &str) -> String
where
F: for<'a> Fn(&'a str) -> &'a str,
{
f(s).to_string()
}You write for<'a> explicitly when:
- the bound is not in
Fnsugar:where for<'a> &'a T: IntoIterator<Item = &'a u32> - you store such closures:
Box<dyn for<'a> Fn(&'a str) -> &'a str>(the sugar works here too) - a trait method takes a reference whose lifetime the implementor must accept for any caller
fn sum_all<T>(c: &T) -> u32
where
for<'a> &'a T: IntoIterator<Item = &'a u32>,
{
c.into_iter().sum()
}
fn main() {
let v = vec![1, 2, 3];
assert_eq!(sum_all(&v), 6);
}A closure written inline does not always infer a higher-ranked signature; |s: &str| s may be inferred with
one specific lifetime, giving "expected signature for<'a> fn(&'a str) -> &'a str". Annotate the parameter
and return type, or pass it straight into a function with the Fn bound so the expected signature is known.
Iterators over borrowed data
An iterator struct borrows its source; the items carry the source's lifetime, not the lifetime of &mut self
in next:
struct Lines<'a> { rest: &'a str }
impl<'a> Iterator for Lines<'a> {
type Item = &'a str; // items outlive each call to next()
fn next(&mut self) -> Option<&'a str> {
if self.rest.is_empty() {
return None;
}
let (line, rest) = self.rest.split_once('\n').unwrap_or((self.rest, ""));
self.rest = rest;
Some(line)
}
}
fn lines(s: &str) -> Lines<'_> { Lines { rest: s } }Returning an iterator from a function: -> impl Iterator<Item = &'a T> + 'a (2021) or plain
-> impl Iterator<Item = &T> (2024, captures automatically). Box<dyn Iterator<Item = &'a T> + 'a> when the
concrete type varies.
Lending iterators with GATs
A lending iterator yields items that borrow the iterator itself (windows over an internal buffer). The
standard Iterator cannot express it because Item has no lifetime parameter. Generic associated types
(Rust 1.65) can:
trait LendingIterator {
type Item<'a>
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
struct WindowsMut<'t, T> { data: &'t mut [T], start: usize, size: usize }
impl<'t, T> LendingIterator for WindowsMut<'t, T> {
type Item<'a> = &'a mut [T] where Self: 'a;
fn next<'a>(&'a mut self) -> Option<&'a mut [T]> {
let end = self.start + self.size;
if end > self.data.len() {
return None;
}
let w = &mut self.data[self.start..end];
self.start += 1;
Some(w)
}
}
fn main() {
let mut v = [1, 2, 3, 4];
let mut it = WindowsMut { data: &mut v, start: 0, size: 2 };
while let Some(w) = it.next() {
w[0] += w[1]; // each window is dropped before the next call
}
assert_eq!(v, [3, 5, 7, 4]);
}The where Self: 'a clause is required on the GAT; the compiler asks for it. No for loop or adapters
come for free: consume with while let. Crates such as lending-iterator add adapters.
Closures that take or return references
// Returning a closure that borrows an argument: tie the closure to the borrow.
fn contains_any<'a>(needles: &'a [&str]) -> impl Fn(&str) -> bool + 'a {
move |hay| needles.iter().any(|n| hay.contains(n))
}
// Returning a reference from a closure parameter needs an explicit signature or a helper:
fn first_word(s: &str) -> &str { s.split(' ').next().unwrap_or("") }
fn main() {
let f = contains_any(&["err", "warn"]);
assert!(f("warn: disk"));
let words: Vec<&str> = ["a b", "c d"].iter().map(|s| first_word(s)).collect();
assert_eq!(words, ["a", "c"]);
}let g = |s: &str| -> &str { s }; fails to infer a higher-ranked signature on stable ("lifetime may not live
long enough"); use a fn item or pass the closure directly to a function whose bound says
Fn(&str) -> &str.
When the checker is too strict
Some sound code is rejected by today's NLL checker; recognise it and use the standard workaround instead of adding lifetimes:
| Shape | Symptom | Workaround |
|---|---|---|
| Return a borrow on one branch, mutate on the other (get-or-insert) | E0499/E0502, "returning this value requires that ..." | entry API; check with contains_key first; return an index |
Cursor loop while let Some(n) = cur.next.as_mut() { cur = n; } | E0499 | test with is_some(), then borrow unconditionally |
&'a mut self method on impl<'a> S<'a> | second call fails | &mut self (LT007) |
| Closure stored in a struct borrowing another field of the struct | self-reference | pass data as a closure argument, or split the struct |
These are covered with full code in the borrow-checker skill; the Polonius analysis (nightly) accepts the first two.
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.