Skip to content

S21: Unnecessary lifetime annotations, or 'static to silence errors

S21Severity: Low

Annotations that elision makes unnecessary, or a 'static bound added to stop an error.

Clippy: needless_lifetimes, elidable_lifetime_names

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
}
}
  • 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. 'static is the one-word fix, and it compiles for the first caller, which uses a literal.
  • Box::leak is a known trick to turn a String into a &'static str. It compiles everywhere, and it is the fix that the next caller’s error leads to.
  • 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 str in 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::leak leaks memory on every call. The memory is never freed. In a server that creates a Customer for each request, memory use grows until the process is stopped. The test worse_static_leaks_one_string_per_call shows that the code compiles and runs, which is why this is easy to miss in a review.
  1. Worse<'a> on a function with one input reference, and <'b> on a method that returns a borrow of self.

    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
    }
    }
  2. 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 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 str is the clearest way to say it.
  • 'static on real constants and literals. const DEFAULT_CURRENCY: &str is implicitly 'static.
  • T: 'static bounds where the API needs them, for example thread::spawn. This means “owns its data”, not “lives forever”. See Lifetimes.
  • Box::leak once at startup, for a value that lives until the program ends anyway (a configuration that many threads read). Write a comment that says so.
  • Clippy needless_lifetimes (complexity group, on by default) reports function lifetimes that elision makes unnecessary.
  • Clippy elidable_lifetime_names (pedantic group) reports impl<'a> Type<'a> where impl Type<'_> is enough.
  • Search for Box::leak, &'static str in struct fields, and 'static in a diff that also fixes a borrow error.
  • No lint can tell if a 'static was added to silence an error. Ask the agent why the struct does not own its data.
Remove the lifetime annotations that elision makes unnecessary (run cargo
clippy and fix needless_lifetimes and elidable_lifetime_names). Change
Customer.name from &'static str to String, and remove the Box::leak call in
customer_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.