ExamplesBy LevelBy TopicLearning Paths
107 Intermediate

107-lifetime-structs — Lifetimes in Structs

Functional Programming

Tutorial

The Problem

When a struct holds a reference, the struct's validity is tied to the data it borrows. A struct holding &str cannot outlive the String it was created from — if the String is dropped, the struct would hold a dangling pointer. Rust's lifetime annotations on structs make this relationship explicit, preventing the struct from being used after its data is freed.

This is the pattern behind zero-copy parsers (structs holding slices into the original input), iterator adapters (structs holding references to collections), and any API that borrows from caller data.

🎯 Learning Outcomes

  • • Annotate a struct with a lifetime parameter <'a>
  • • Understand that the struct lives at most as long as the borrowed data
  • • Use lifetime annotations in impl blocks
  • • Handle multiple borrowed fields sharing or having independent lifetimes
  • • Connect to zero-copy parsing patterns in nom, winnow, and serde
  • Code Example

    #[derive(Debug)]
    struct Excerpt<'a> {
        text: &'a str,
        page: u32,
    }
    
    fn main() {
        let book = String::from("Call me Ishmael. Some years ago...");
        let exc = Excerpt { text: &book[..16], page: 1 };
        assert_eq!(exc.text, "Call me Ishmael.");
        println!("Excerpt p.{}: {}", exc.page, exc.text);
        // Compiler guarantees: exc cannot outlive book
    }

    Key Differences

  • Lifetime annotation burden: Rust requires explicit <'a> on any struct holding a reference; OCaml requires no annotations.
  • Zero-copy semantics: Rust's lifetime annotation makes zero-copy safe — the compiler proves the slice is valid; OCaml's GC ensures validity at runtime.
  • Multiple lifetimes: Rust structs can have multiple independent lifetime parameters (<'a, 'b>); OCaml has no equivalent.
  • Compile-time vs runtime: Rust enforces borrowing at compile time with zero runtime overhead; OCaml's GC enforces memory safety at runtime.
  • OCaml Approach

    OCaml structs holding string references have no equivalent constraint — the GC keeps everything alive:

    type excerpt = { text: string; page: int }
    
    let make_excerpt text page = { text; page }
    (* text can be freed by OCaml's GC only when all references are dropped *)
    

    An OCaml excerpt can outlive any particular binding to the original string because the GC tracks reference counts. There is no concept of "borrows from" in OCaml's type system.

    Full Source

    #![allow(clippy::all)]
    // Example 107: Lifetimes in Structs
    //
    // When a struct holds a reference, it needs a lifetime parameter
    // to ensure the referenced data outlives the struct.
    
    // Approach 1: Idiomatic Rust — struct borrowing a string slice
    // The lifetime 'a ties the struct's validity to the borrowed data.
    #[derive(Debug)]
    pub struct Excerpt<'a> {
        pub text: &'a str,
        pub page: u32,
    }
    
    impl<'a> Excerpt<'a> {
        pub fn new(text: &'a str, page: u32) -> Self {
            Excerpt { text, page }
        }
    
        pub fn announce(&self, announcement: &str) -> &str {
            println!("Attention: {}", announcement);
            self.text
        }
    }
    
    // Approach 2: Struct with multiple borrowed fields
    // All fields share the same lifetime 'a — the struct is valid for
    // exactly as long as the shortest-lived of its borrowed data.
    #[derive(Debug)]
    pub struct Article<'a> {
        pub title: &'a str,
        pub author: &'a str,
        pub body: &'a str,
    }
    
    impl<'a> Article<'a> {
        pub fn new(title: &'a str, author: &'a str, body: &'a str) -> Self {
            Article {
                title,
                author,
                body,
            }
        }
    
        pub fn summarize(&self) -> String {
            format!(
                "{} by {} ({} chars)",
                self.title,
                self.author,
                self.body.len()
            )
        }
    }
    
    // Approach 3: Struct with a lifetime-bound method returning a reference
    // The returned &str borrows from self, so it cannot outlive the struct.
    #[derive(Debug)]
    pub struct Parser<'a> {
        input: &'a str,
        pos: usize,
    }
    
    impl<'a> Parser<'a> {
        pub fn new(input: &'a str) -> Self {
            Parser { input, pos: 0 }
        }
    
        /// Returns the remaining unparsed input — a sub-slice of the original.
        pub fn remaining(&self) -> &'a str {
            &self.input[self.pos..]
        }
    
        /// Advance past the next `n` bytes (ASCII only for simplicity).
        pub fn advance(&mut self, n: usize) {
            self.pos = (self.pos + n).min(self.input.len());
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_excerpt_borrows_slice() {
            let book = String::from("Call me Ishmael. Some years ago...");
            let exc = Excerpt::new(&book[..16], 1);
            assert_eq!(exc.text, "Call me Ishmael.");
            assert_eq!(exc.page, 1);
        }
    
        #[test]
        fn test_excerpt_announce_returns_text() {
            let sentence = "Fear is the mind-killer.";
            let exc = Excerpt::new(sentence, 42);
            let returned = exc.announce("test");
            assert_eq!(returned, sentence);
        }
    
        #[test]
        fn test_article_summarize() {
            let title = "Rust Ownership";
            let author = "Alice";
            let body = "Ownership is the key to Rust's safety guarantees.";
            let article = Article::new(title, author, body);
            let summary = article.summarize();
            assert!(summary.contains("Rust Ownership"));
            assert!(summary.contains("Alice"));
            assert!(summary.contains(&body.len().to_string()));
        }
    
        #[test]
        fn test_article_fields_accessible() {
            let article = Article {
                title: "Zero Cost",
                author: "Bob",
                body: "Abstractions without overhead.",
            };
            assert_eq!(article.title, "Zero Cost");
            assert_eq!(article.author, "Bob");
        }
    
        #[test]
        fn test_parser_remaining_advances() {
            let input = "fn main() {}";
            let mut parser = Parser::new(input);
            assert_eq!(parser.remaining(), "fn main() {}");
            parser.advance(3);
            assert_eq!(parser.remaining(), "main() {}");
        }
    
        #[test]
        fn test_parser_advance_clamps_to_end() {
            let input = "hello";
            let mut parser = Parser::new(input);
            parser.advance(100);
            assert_eq!(parser.remaining(), "");
        }
    
        #[test]
        fn test_excerpt_debug_format() {
            let text = "To be or not to be";
            let exc = Excerpt::new(text, 7);
            let debug = format!("{:?}", exc);
            assert!(debug.contains("To be or not to be"));
            assert!(debug.contains('7'));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_excerpt_borrows_slice() {
            let book = String::from("Call me Ishmael. Some years ago...");
            let exc = Excerpt::new(&book[..16], 1);
            assert_eq!(exc.text, "Call me Ishmael.");
            assert_eq!(exc.page, 1);
        }
    
        #[test]
        fn test_excerpt_announce_returns_text() {
            let sentence = "Fear is the mind-killer.";
            let exc = Excerpt::new(sentence, 42);
            let returned = exc.announce("test");
            assert_eq!(returned, sentence);
        }
    
        #[test]
        fn test_article_summarize() {
            let title = "Rust Ownership";
            let author = "Alice";
            let body = "Ownership is the key to Rust's safety guarantees.";
            let article = Article::new(title, author, body);
            let summary = article.summarize();
            assert!(summary.contains("Rust Ownership"));
            assert!(summary.contains("Alice"));
            assert!(summary.contains(&body.len().to_string()));
        }
    
        #[test]
        fn test_article_fields_accessible() {
            let article = Article {
                title: "Zero Cost",
                author: "Bob",
                body: "Abstractions without overhead.",
            };
            assert_eq!(article.title, "Zero Cost");
            assert_eq!(article.author, "Bob");
        }
    
        #[test]
        fn test_parser_remaining_advances() {
            let input = "fn main() {}";
            let mut parser = Parser::new(input);
            assert_eq!(parser.remaining(), "fn main() {}");
            parser.advance(3);
            assert_eq!(parser.remaining(), "main() {}");
        }
    
        #[test]
        fn test_parser_advance_clamps_to_end() {
            let input = "hello";
            let mut parser = Parser::new(input);
            parser.advance(100);
            assert_eq!(parser.remaining(), "");
        }
    
        #[test]
        fn test_excerpt_debug_format() {
            let text = "To be or not to be";
            let exc = Excerpt::new(text, 7);
            let debug = format!("{:?}", exc);
            assert!(debug.contains("To be or not to be"));
            assert!(debug.contains('7'));
        }
    }

    Deep Comparison

    OCaml vs Rust: Lifetimes in Structs

    Side-by-Side Code

    OCaml

    (* OCaml structs own their data — no lifetime needed *)
    type excerpt = { text : string; page : int }
    
    let make_excerpt text page = { text; page }
    
    let () =
      let book = "Call me Ishmael. Some years ago..." in
      let exc = make_excerpt (String.sub book 0 16) 1 in
      assert (exc.text = "Call me Ishmael.");
      Printf.printf "Excerpt p.%d: %s\n" exc.page exc.text
    

    Rust (idiomatic — struct borrows data)

    #[derive(Debug)]
    struct Excerpt<'a> {
        text: &'a str,
        page: u32,
    }
    
    fn main() {
        let book = String::from("Call me Ishmael. Some years ago...");
        let exc = Excerpt { text: &book[..16], page: 1 };
        assert_eq!(exc.text, "Call me Ishmael.");
        println!("Excerpt p.{}: {}", exc.page, exc.text);
        // Compiler guarantees: exc cannot outlive book
    }
    

    Rust (functional — struct with multiple borrowed fields)

    #[derive(Debug)]
    struct Article<'a> {
        title: &'a str,
        author: &'a str,
        body: &'a str,
    }
    
    impl<'a> Article<'a> {
        fn summarize(&self) -> String {
            format!("{} by {} ({} chars)", self.title, self.author, self.body.len())
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    Struct with stringtype t = { text: string }struct T<'a> { text: &'a str }
    Constructorlet make t p = { text=t; page=p }fn new(text: &'a str, page: u32) -> Self
    Method returning borrowreturns owned stringfn get(&self) -> &'a str
    Lifetime parameterimplicit (GC manages)explicit 'a on struct and impl

    Key Insights

  • Ownership vs GC: In OCaml, string fields are heap-allocated and reference-counted by the GC — the struct owns a copy. In Rust, &'a str is a borrow: the struct holds a pointer without owning the data, so it must be proven valid.
  • Explicit lifetime parameter: The 'a on Excerpt<'a> is Rust's way of encoding "this struct is only valid while the referenced str is alive." OCaml's GC makes this implicit — any live reference prevents collection.
  • Compiler-enforced scope: Rust will reject code that moves Excerpt to a scope where the referenced String is no longer live. OCaml, Java, and Python silently keep the source alive (or crash in C). Rust does this at zero runtime cost.
  • Zero-copy views: Because &'a str is a slice into existing memory, creating an Excerpt never allocates. The OCaml equivalent (String.sub) copies bytes. Lifetimes make zero-copy safe without any runtime bookkeeping.
  • Multiple lifetime parameters: A struct can carry multiple lifetimes (struct Pair<'a, 'b>) when its fields borrow from different sources with potentially different scopes — something invisible in GC languages but explicit and precise in Rust.
  • When to Use Each Style

    **Use borrowing structs (&'a str) when:** you want zero-copy views into existing data — parsing, tokenizing, window operations, or any case where you'd otherwise copy a substring just to store it temporarily.

    **Use owned structs (String) when:** the struct needs to outlive its source, be sent across threads, or stored in a collection that must own its data. The tradeoff is an allocation, but you gain unrestricted lifetime.

    Exercises

  • Create a ParseResult<'a> { input: &'a str, consumed: &'a str, remaining: &'a str } struct for a simple parser and implement a parse_word function.
  • Write a Config<'a> struct that borrows from a &'a str configuration file content and provides methods to look up values.
  • Demonstrate the lifetime error when trying to store an Excerpt<'a> in a Vec that outlives the source string.
  • Open Source Repos