S21: Unnecessary lifetime annotations, or 'static to silence errors
What it looks like
Section titled “What it looks like”Annotations that the compiler would infer:
pub fn first_word<'a>(text: &'a str) -> &'a str { text.split_whitespace().next().unwrap_or("")}
pub struct Summary<'a> { pub customer: &'a str, pub total_cents: u64,}
impl<'a> Summary<'a> { pub fn customer<'b>(&'b self) -> &'b str { self.customer }}And 'static used to make an error go away:
pub struct Customer { pub name: &'static str, // `'static` added to stop "missing lifetime specifier"}
pub fn customer_from_input(line: &str) -> Customer { let name = line.trim().to_string(); Customer { name: Box::leak(name.into_boxed_str()), // never freed }}Why AI agents produce it
Section titled “Why AI agents produce it”- Explicit looks safe. An agent that is not sure about elision writes every lifetime out. The code compiles, so the agent does not learn that they are unnecessary.
- The compiler error mentions lifetimes. “missing lifetime specifier” (E0106) on a struct field suggests adding
<'a>, or'static.'staticis the one-word fix, and it compiles for the first caller, which uses a literal. Box::leakis a known trick to turn aStringinto a&'static str. It compiles everywhere, and it is the fix that the next caller’s error leads to.
Why it is a problem
Section titled “Why it is a problem”- Unnecessary annotations are noise with a false signal. Explicit lifetimes tell the reader “the relation between these references is unusual”. When it is not unusual, the reader looks for a problem that is not there.
&'static strin a struct limits it to literals. Every value that comes from input, a file, or a database cannot go in without a leak or a redesign.Box::leakleaks memory on every call. The memory is never freed. In a server that creates aCustomerfor each request, memory use grows until the process is stopped. The testworse_static_leaks_one_string_per_callshows that the code compiles and runs, which is why this is easy to miss in a review.
The better version
Section titled “The better version”Worse
<'a>on a function with one input reference, and<'b>on a method that returns a borrow ofself.pub fn first_word<'a>(text: &'a str) -> &'a str {text.split_whitespace().next().unwrap_or("")}pub struct Summary<'a> {pub customer: &'a str,pub total_cents: u64,}impl<'a> Summary<'a> {pub fn customer<'b>(&'b self) -> &'b str {self.customer}}BetterElision gives the same meaning.
impl Summary<'_>says "a lifetime, but its name does not matter".pub fn first_word(text: &str) -> &str {text.split_whitespace().next().unwrap_or("")}pub struct Summary<'a> {pub customer: &'a str,pub total_cents: u64,}impl Summary<'_> {pub fn customer(&self) -> &str {self.customer}}
For the 'static field, own the data:
pub struct Customer { pub name: String,}
pub fn customer_from_input(line: &str) -> Customer { Customer { name: line.trim().to_string(), }}The cost is one allocation for each customer, which the leaking version also had. The difference is that this one is freed.
When it is acceptable
Section titled “When it is acceptable”- When elision cannot decide. Two input references and a reference output need an annotation. So do structs that hold references:
/// Two inputs and one output: the compiler cannot guess which input the/// result borrows from, so the annotation is required.pub fn longer_name<'a>(first: &'a str, second: &'a str) -> &'a str { if second.len() > first.len() { second } else { first }}
/// A real constant: a string literal is `&'static str`.pub const DEFAULT_CURRENCY: &str = "EUR";- When the annotation documents a choice, for example a function with two reference inputs where the result borrows only from the first. Then
fn f<'a>(x: &'a str, y: &str) -> &'a stris the clearest way to say it. 'staticon real constants and literals.const DEFAULT_CURRENCY: &stris implicitly'static.T: 'staticbounds where the API needs them, for examplethread::spawn. This means “owns its data”, not “lives forever”. See Lifetimes.Box::leakonce at startup, for a value that lives until the program ends anyway (a configuration that many threads read). Write a comment that says so.
How to detect it
Section titled “How to detect it”- Clippy
needless_lifetimes(complexity group, on by default) reports function lifetimes that elision makes unnecessary. - Clippy
elidable_lifetime_names(pedantic group) reportsimpl<'a> Type<'a>whereimpl Type<'_>is enough. - Search for
Box::leak,&'static strin struct fields, and'staticin a diff that also fixes a borrow error. - No lint can tell if a
'staticwas added to silence an error. Ask the agent why the struct does not own its data.
What to tell your agent
Section titled “What to tell your agent”Remove the lifetime annotations that elision makes unnecessary (run cargoclippy and fix needless_lifetimes and elidable_lifetime_names). ChangeCustomer.name from &'static str to String, and remove the Box::leak call incustomer_from_input. Do not add 'static or Box::leak to fix a borrow error;change who owns the value instead.Related: S01: clone() to satisfy the borrow checker, and Lifetimes.