You have a struct. It holds a &str. The compiler says E0106. You add <'a>.
Now the struct that contains that struct needs <'a>. Then the function that
returns it. Then the trait. Then the Vec you wanted to put them in stops
compiling for reasons you cannot parse, and somewhere around here a reasonable
person concludes Rust is unusable for real data structures.
That spread is real, it has a name, and it has a decision rule that prevents it almost entirely. But first the mechanics.
The declaration
struct ImportantExcerpt<'a> {
part: &'a str,
}
Read the <'a> as a question the type asks its users: “for how long are the
references I hold valid?” Every use of the type answers it.
What the declaration guarantees is one sentence:
An instance of
ImportantExcerpt<'a>cannot outlive the data itspartfield points at.
Which is exactly the property you want, and the compiler enforces it without any further help from you:
fn main() {
let excerpt;
{
let novel = String::from("Call me Ishmael. Some years ago...");
excerpt = ImportantExcerpt { part: &novel[..15] };
}
println!("{}", excerpt.part);
// error[E0597]: `novel` does not live long enough
}
The lifetime is part of the type
This is the fact that makes everything else make sense, and it is easy to skate
past: ImportantExcerpt is not a type. ImportantExcerpt<'a> is. Different
lifetimes give you different types, related only by variance — which is why the
lifetime has to appear on the impl header too:
impl<'a> ImportantExcerpt<'a> {
fn part(&self) -> &str { self.part }
}
Leave it off and you do not get a helpful nudge, you get a distinct error:
impl ImportantExcerpt {
// ^ error[E0726]: implicit elided lifetime not allowed here
// expected lifetime parameter
}
When the methods do not care which lifetime it is, impl ImportantExcerpt<'_>
is the idiomatic spelling — it says “there is a lifetime here, I am not using
its name”. Prefer it; clippy::needless_lifetimes will nudge you toward it when
the named version is doing nothing.
💡struct Foo<'a> { .. } — does that mean "a Foo lives for 'a"? Get this wrong and variance and drop-checking will never make sense.
click to reveal
No. It means “a Foo<'a> contains references that are valid for at least 'a.”
The distinction sounds pedantic and is not. Consider what each reading predicts:
-
“Foo lives for ‘a” predicts that a
Foo<'long>and aFoo<'short>are somehow incomparable, or that creating aFoo<'static>makes it live forever. -
“Foo contains references valid for at least ‘a” predicts that a
Foo<'long>can be used anywhere aFoo<'short>is expected — because references good for a long region are certainly good for a shorter one.
The second is what actually happens: Foo<'a> is covariant in 'a (given a plain &'a T field), so Foo<'long> is a subtype of Foo<'short>. That is the whole reason ordinary code works — every time you pass a struct built from long-lived data into a function expecting a shorter-lived one, this rule fires silently.
It also sets up drop-checking. The compiler has to reason about whether a Foo<'a>‘s destructor could observe the referent, and “contains references valid for at least 'a“ is the statement that reasoning is built on. “Foo lives for ‘a” would be a claim about the container’s storage, which the type system does not track at all.
The framing that stops the spread
Here it is:
A lifetime-parameterised struct is a view, not a container.
A Vec<String> is a container: it owns its contents and can go anywhere. A
Parser<'a>, a Tokens<'a>, an ImportantExcerpt<'a> — these are windows
onto data someone else owns. They are cheap, they are temporary, and they are
tied to the thing they look at.
Once you see it that way, the viral behaviour stops being mysterious: a view cannot be stored inside a container that outlives the source, because it is a view. Any type that holds a view is itself a view. The parameter spreads because the property spreads, and the type system is reporting a real constraint rather than inventing a bureaucratic one.
The decision rule
Borrow when the struct is:
- short-lived and local to a parse, scan or traversal;
- created, used and dropped inside one function or one call chain;
- performance-relevant enough that avoiding the copy matters.
Own when the struct must be:
- stored in a collection that outlives the source data;
- returned upward past the scope that owns the source;
-
sent across threads or into a
'staticcontext (a spawned thread, a boxed trait object, an event handler registry); - kept in a long-lived cache or config.
If you find yourself adding <'a> to a type that will be stored somewhere,
stop: that is the signal to own instead. The rule is not “borrowing is faster so
borrow”; it is “borrow for views, own for storage”.
💡You are writing a log analyser. It parses lines and produces one record per line, and the caller wants all the records back in a Vec to sort and query afterwards. Should Record borrow from the input, or own its strings?
click to reveal
struct Record<'a> { level: &'a str, message: &'a str }
// ...or...
struct Record { level: String, message: String }
It depends on one thing: does the input text outlive the Vec<Record>?
If the caller reads the whole file into a String first, keeps it alive, and only then parses — Record<'a> is not just fine, it is clearly better. Millions of records with zero allocation, all pointing into one buffer. This is exactly how fast log and CSV parsers are written.
If the records are produced line-by-line from a streaming reader, or returned from a function that owns the buffer, or handed to another thread — the borrowing version cannot work, and no annotation will make it work. Own the strings.
The middle case is the interesting one, and it is where Cow<'a, str> earns its keep: borrow when you can, own when you must, one type either way.
Notice how the decision was made. Not “which is more idiomatic”, not “which is faster” — but “what has to outlive what”. That is the question a lifetime parameter is asking, so it is the question to answer.
The escape hatches, when a view will not do
When the analysis above says “own”, you have three tools:
1. Own the data. String instead of &str, Vec<T> instead of &[T].
Costs an allocation and a copy; buys you a type with no lifetime parameter that
can go anywhere. This is the right answer far more often than people who have
just learned about zero-copy want it to be.
2. Rc<T> / Arc<T>. Shared ownership with a reference count. Several
owners, no lifetime parameter, the data dies when the last owner does. Costs a
counter bump and a pointer chase; buys you a graph shape the borrow checker
cannot otherwise express. Arc for across threads, Rc for within one.
3. Indices into a separately-owned arena. Keep all the data in one
Vec<T> that somebody clearly owns, and have your structs hold usize
indices instead of references. This is how most serious tree, graph and AST
implementations in Rust are built. Costs you bounds checks and the ability to
dereference without the arena in hand; buys you a fully 'static, trivially
serialisable, Clone-able data structure with no borrow-checker involvement
at all.
One lint to know about
When you write a struct with both a lifetime and a type parameter, you may be tempted to spell the relationship out:
pub struct S<'a, T: 'a> { d: &'a T }
rustc’s explicit_outlives_requirements lint (allow-by-default, so you will
only see it if you turn it on) says:
warning: outlives requirements can be inferred
|
2 | pub struct S<'a, T: 'a> { d: &'a T }
| ^^^^ help: remove this bound
The T: 'a is implied by the field type — a &'a T field cannot exist
unless T: 'a — so writing it adds nothing. Implied bounds are worth knowing
about in general; they get their own article.
The summary
-
struct Foo<'a>means “contains references valid for at least'a“, not “lives for'a“. -
Foo<'a>is the type; the parameter goes on theimplheader too. - A lifetime-parameterised struct is a view. Views cannot be stored past the thing they view.
- Borrow for parse-and-scan; own for storage, for returning upward, for threads, for collections that outlive the source.
-
When you must own:
String/Vec,Rc/Arc, or indices into an arena.