Skip to content
← All articles

Structs that hold references

The second stall point after E0106: you add `<'a>` to a struct, watch it metastasise through your whole program, and conclude Rust cannot express real data structures. Here is the framing that stops the spread — a lifetime-parameterised struct is a view, not a container.

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 its part field 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 a Foo<'short> are somehow incomparable, or that creating a Foo<'static> makes it live forever.
  • “Foo contains references valid for at least ‘a” predicts that a Foo<'long> can be used anywhere a Foo<'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 'static context (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 the impl header 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.