---
title: 'References in Structs'
source: 'https://academia.sh/en/courses/ownership/references-in-structs'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Explains that holding a reference in a struct's field requires a lifetime parameter"
    - 'Runs code to compare the end point of a moved field with that of a borrowed field'
    - 'Shows with a rejection-acceptance pair that a struct cannot outlive the data it refers to'
---

# References in Structs

Measures the condition that holding a reference in a struct's field places on the struct's own life: a struct cannot outlive the data it refers to. The same data is held both by moving it into a struct and by borrowing it into one; it is compared by running the code that a moved field ends together with the struct, while borrowed data ends after the struct.

The previous lesson measured which signatures elision covers and which it does not among
function parameters. But an explicit annotation can be required not only in function
signatures but also in a struct's own definition: if a struct's field holds a reference, the
struct itself becomes tied to that reference's life. This lesson measures this condition by
comparing two designs that hold the same data in a struct through two different paths — by
moving it and by borrowing it.

The question the Go curriculum's Concurrency course asks is different here too: there, a
running unit's stopping was measured; here, the end point of a field inside a struct is
measured. The four end classes established in the Stack and Heap lesson show up again here,
but this time compared across two different field kinds.

Throughout this topic the thing measured has been the same question: where is a reference's
validity range read from? The Lifetime Concept lesson measured this range for a reference, and
the Explicit Lifetime Annotations and Lifetime Elision Rules lessons measured it for a
function signature. This final lesson carries the same question into a struct definition: how
does a struct's **own** life relate to the life of a reference it holds.

## Two Paths: Moving and Borrowing

When a struct wants to put a value into one of its fields, it has two options: it can **move**
the value (the field becomes the owner) or it can hold a **reference** to the value (the field
only borrows it). The move rule seen in the Ownership and Moves lesson applies to the first
option — the field behaves like any ordinary owned value on its own. The second option opens a
new question: how long can the struct itself live, can it live longer than the data it refers
to?

The answer is no, and this is a rule the compiler makes mandatory in a struct definition. If a
struct's field holds a reference, the struct's own definition has to carry a lifetime
parameter — this parameter relates how long the struct itself can live, at most, to the data
it refers to.

This is the struct-definition counterpart of the rule established for function signatures in
the previous two lessons. There, a function had to say which input its returned reference was
tied to; here, the struct itself has to say which source the reference it holds is tied to.
The difference is where the relationship is established: not in the life of a function call,
but in the life of a struct instance. No matter how long that instance lives — in a variable,
in another struct's field, inside a collection — the guarantee that it cannot outlive the
source of the reference it holds does not change.

## Measurement: A Moved Field Ends With the Struct, Borrowed Data Ends After the Struct

The measurement below defines two structs: `OwningStruct` owns a `Trace` value,
`BorrowingStruct` only holds a reference to a `Trace` value and, for that reason, carries an
`<'a>` parameter. Both structs operate on the same core (`Trace`, `Ledger`, `Drop`); the only
difference measured is whether the field is moved or not.

```rust
// reference_in_struct.rs — comparing the end point of a moved field in a struct with that of a borrowed field.

use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

fn read(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}

fn newly_added(ledger: &Ledger, before: usize) -> String {
    let all = read(ledger);
    all[before..].join(", ")
}

struct OwningStruct {
    content: Trace,
}

struct BorrowingStruct<'a> {
    content: &'a Trace,
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    // Moved field: ends when the struct ends.
    let s0 = read(&ledger).len();
    {
        let moved = Trace::new("moved-field", &ledger);
        let instance = OwningStruct { content: moved };
        println!("field inside OwningStruct -> {}", instance.content.name);
        println!("ledger while OwningStruct stands -> [{}]", newly_added(&ledger, s0));
    }
    println!("once OwningStruct ends  -> {}", newly_added(&ledger, s0));

    // Borrowed field: the struct ends, the data still lives.
    let o0 = read(&ledger).len();
    {
        let borrowed_data = Trace::new("borrowed-data", &ledger);
        {
            let instance = BorrowingStruct { content: &borrowed_data };
            println!("inside BorrowingStruct  -> {}", instance.content.name);
        }
        println!("once BorrowingStruct ends -> [{}]", newly_added(&ledger, o0));
    }
    println!("once borrowed-data ends -> {}", newly_added(&ledger, o0));
}
```

```
field inside OwningStruct -> moved-field
ledger while OwningStruct stands -> []
once OwningStruct ends  -> moved-field
inside BorrowingStruct  -> borrowed-data
once BorrowingStruct ends -> []
once borrowed-data ends -> borrowed-data
```

In the `OwningStruct` section, the `moved` value **moves** into `instance`'s field, and from
that point on it has no binding of its own — it is reachable only through `instance.content`.
While `instance` stands, the ledger is empty; once `instance`'s scope closes, `moved-field`
drops into the ledger. This repeats the "value moved into a struct field" pattern measured in
the Ownership and Moves lesson: the field ends together with the struct itself.

In the `BorrowingStruct` section, `borrowed_data` stays in its own binding and `instance` only
holds a reference to it. When `instance`'s own inner scope closes, the ledger is **still
empty** — the end of the `BorrowingStruct` instance produces no record at all, because
`&'a Trace` does not implement `Drop`. `borrowed_data` itself only drops into the ledger once
its own scope (the outer block) closes — and that is **after** the `BorrowingStruct` instance
ends.

What the two sections have in common is that only one record is produced in each: the sole
record in the `OwningStruct` section is `moved-field`, the sole record in the
`BorrowingStruct` section is `borrowed-data`. `BorrowingStruct` itself never drops into the
ledger — because the struct does not implement `Drop`, it only looks, through the reference it
holds, at a `Trace` value living elsewhere. This shows that a struct holding a reference has
no observable side effect from its own end; the only thing observable is the end of the data
it refers to, and that end never comes before the struct's own end — at the earliest, the two
can coincide by ending together in the same scope.

**LI9.** A value moved into a struct's field ends together with the struct itself. The data
behind a reference held in a struct's field, by contrast, ends **after** the struct — the
struct itself never outlives its own data, but the data it refers to can, and usually does,
outlive the struct.

## The Struct's Own Method Is Still Written With Elision

Although `BorrowingStruct<'a>` carries a lifetime parameter, the methods written on it are not
necessarily required to repeat that parameter. The elision rules established in the previous
lesson also apply here: the reference returned by a method taking `&self` is tied to `self`'s
own life by the third rule, and this makes writing `'a` separately unnecessary. This is a
concrete example showing that the two lessons are not independent of each other — the same
rules are applied again and again in different contexts.

```rust
// method_elision.rs — a method on a struct carrying a lifetime parameter is still written with elision.

use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

struct BorrowingStruct<'a> {
    content: &'a Trace,
}

impl<'a> BorrowingStruct<'a> {
    // Without annotation: elision's third rule (self) applies here too.
    fn name(&self) -> &str {
        self.content.name.as_str()
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));
    let data = Trace::new("shared-data", &ledger);
    let instance = BorrowingStruct { content: &data };
    println!("{}", instance.name());
}
```

```
shared-data
```

The `impl<'a> BorrowingStruct<'a>` block has to introduce `'a` — the struct definition
requires it — but the `fn name(&self) -> &str` signature on its own carries no annotation at
all. The compiler behaves here just as it did in the previous lesson: since `&self` is the
single reference input, the third rule applies, and the returned `&str` is tied to `self`'s
life. Because `self` is itself a reference to `BorrowingStruct<'a>`, the returned slice also
stays, indirectly, within `'a`'s bounds — but this relationship is never written in the `name`
method's own signature, because elision already covers it.

This distinction shows the difference between where `'a` is defined and where it is **used**.
`'a` has to appear in the struct's definition and in the `impl` block's header, because the
field itself is tied to it. But if a method's returned value is always reached through `self`,
that method's own signature behaves correctly without writing `'a` again — the elision rule
here works through a single input (`self`) and does not concern itself with `'a` at all. The
place where `'a` would need to be written explicitly would be if a method's returned reference
were tied to a source **other than** `self` (for example, another reference given to the
method as a separate parameter) — which means the previous lesson's limiting measurement
applies here too.

**LI10.** A struct definition carrying a lifetime parameter does not mean every method on
that struct must also carry an explicit annotation. Elision rules operate on struct methods
too; the parameter is needed only in the struct's **definition**, for the field itself.

## Limiting Measurement: A Struct Definition Cannot Be Written Without a Lifetime Parameter

The `BorrowingStruct` definition cannot be written without the `<'a>` parameter. A field
written as `content: &Trace`, with no lifetime specified at all, is rejected by the compiler,
because the struct definition nowhere says which source the reference is tied to or that the
struct will not outlive that source. This is the struct-definition counterpart of the pattern
seen in the previous two lessons: a reference cannot exist without its relationship being
written into the source.

Elision rules do not come into play here at all — the three rules seen in the previous lesson
were defined only for elided references in function signatures, not for struct fields. A
struct's field is neither an input nor an output; a struct is not a signature re-evaluated on
every call, it is a data layout that stands on its own. There is no "single correct reading"
the compiler could assume here either: if a field holds a reference, that reference's source
has to be **outside** the struct, and who that source is, is information only the person
writing it can know.

The accepted form is the `BorrowingStruct<'a>` above — the `<'a>` parameter is added to the
struct itself, the field becomes `&'a Trace`, and the compiler verifies this relationship: no
instance of `BorrowingStruct` can outlive the data `content` points to. The measurement above
stayed within this boundary — `instance` always ended before `borrowed_data`, never after.
Suppose the `instance` value had been moved outside its own inner scope, returned, or assigned
to a variable in the outer scope: that would not compile, because the reference the instance
holds cannot outlive `borrowed_data` — the restriction is the struct-field form of the
boundary run in the Borrow Checker lesson.

**LI11.** If a struct has a field carrying a reference, it cannot be defined without a
lifetime parameter. Once the parameter is added and the relationship is written, the struct
becomes usable, but no instance of that struct can outlive the data it refers to — the
parameter does not remove this boundary, it only writes it into the source.

## Two Designs, One Design Decision

The choice between `OwningStruct` and `BorrowingStruct` is not a question of correctness, it
is a design decision. A moved field makes the struct independent of the data: once
`OwningStruct` is built, it can be moved, returned, or stored for a long time, without caring
whether the data lives anywhere else. The cost of this is a copy or a move — the data now
belongs only to that struct.

A borrowed field, by contrast, makes sharing the data possible: the same `Trace` value can be
used both in its own binding and read through `BorrowingStruct`, without producing a copy at
all. The cost of this is that the struct's own life becomes tied to the data — since
`BorrowingStruct` cannot outlive its data, using it as a return value or storing it for a long
time is generally more restricted. Which design to choose depends on how long the struct will
live and whether the data will also be used elsewhere.

There is no third path between these two options — a field either owns or it borrows, there
is no middle ground. A field can also have write access to the data it borrows
(`&'a mut Trace`), but this still falls into the borrowing category: the struct still does not
own the data, it only holds a reference carrying permission to modify it, and it is still
subject to the same lifetime condition. The shared-ownership tools seen further on in the
course (heap-allocated single ownership, reference counting) may look like a third path
standing between these two options, but they are not — they too are, at bottom, a form of
ownership, only how the ownership rule is applied differs. Holding a reference remains the one
case where the data **never** passes into the struct.

## Summary

- If a struct's field holds a reference, the struct definition must carry a lifetime
  parameter; a definition written without this parameter is rejected.
- A value moved into a struct's field ends together with the struct itself: its life ends
  **where it was moved**.
- The data behind a reference held in a struct's field ends after the struct itself — a
  struct can never outlive its data.
- The lifetime parameter does not remove this boundary, it only writes it into the source;
  even once the parameter is added, a struct instance cannot outlive the data it refers to.
- The choice between a moved field and a borrowed field is a design decision: one makes the
  struct independent of the data, the other makes sharing the data possible.
- The lifetime parameter is required only in the struct's definition; the methods of a struct
  carrying the parameter may need no annotation of their own, as long as elision rules apply.

## Next Step

Throughout this course's first two topics, a value either moved into a struct or was borrowed
by one, and in both cases that value had **a single owner** — the owner of a moved field was
the struct, the owner of borrowed data was its own scope; even in the borrowed case, ownership
was never split, only shared temporarily. The next topic removes this assumption: can a value
have more than one owner, and if so, what does "ending" mean? Beginning with heap-allocated
single ownership, this topic first measures a single heap ownership that makes recursive types
possible, then measures genuinely shared ownership through reference counting.
