ExamplesBy LevelBy TopicLearning Paths
899 Intermediate

899-lifetime-basics — Lifetime Basics

Functional Programming

Tutorial

The Problem

A dangling pointer — a reference to memory that has been freed — is one of the most common and dangerous bugs in C/C++. Rust prevents dangling pointers through lifetimes: compile-time annotations that track how long a reference remains valid. When a function returns a reference, the compiler needs to know whether it comes from parameter a, parameter b, or neither. Explicit lifetime parameters 'a provide this information. OCaml's GC prevents dangling pointers at runtime; Rust prevents them at compile time with zero runtime cost. Lifetimes are the mechanism behind Rust's memory safety guarantee.

🎯 Learning Outcomes

  • • Read and write lifetime annotations on function signatures
  • • Understand that lifetimes express relationships between reference lifetimes, not their duration
  • • Use lifetime annotations in structs that hold references
  • • Understand lifetime elision rules for common single-reference patterns
  • • Compare Rust's compile-time lifetime tracking with OCaml's GC-based approach
  • Code Example

    // 'a names the overlap of both input lifetimes.
    // The returned reference cannot outlive either argument.
    pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
        if a.len() >= b.len() { a } else { b }
    }

    Key Differences

  • Compile-time vs runtime: Rust lifetimes enforce safety at compile time (zero overhead); OCaml's GC enforces it at runtime (GC overhead).
  • Annotation burden: Rust requires lifetime annotations when the compiler cannot infer them; OCaml requires no annotations — the GC handles it.
  • Elision rules: Rust elides lifetimes in common patterns (single reference input, &self methods); when elision doesn't apply, explicit 'a is required.
  • Struct lifetimes: Rust structs containing references need lifetime parameters; OCaml records can contain any values freely.
  • OCaml Approach

    OCaml has no lifetime annotations. All values are heap-allocated and GC-managed — there is no concept of a value "going out of scope" while it has a live reference. Functions returning references to parameters are impossible in the C sense; OCaml functions always return GC-managed values. The equivalent safety guarantee comes from the GC: no value is freed while any reference to it exists. The cost is GC overhead; the benefit is no explicit lifetime management.

    Full Source

    #![allow(clippy::all)]
    // Example 899: Lifetime Basics
    //
    // Lifetimes ensure references don't outlive the data they point to.
    // OCaml's GC handles this automatically; Rust proves it at compile time.
    
    // Approach 1: Idiomatic — lifetime annotation on a function
    // Tells the compiler: the returned reference lives no longer than both inputs.
    pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
        if a.len() >= b.len() {
            a
        } else {
            b
        }
    }
    
    // Approach 2: Returning a reference tied to a single input
    // 'a says: the returned &str borrows from `s`, not from `prefix`.
    pub fn trim_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
        s.strip_prefix(prefix).unwrap_or(s)
    }
    
    // Approach 3: Struct with a lifetime
    // The struct cannot outlive the string slice it holds.
    pub struct Excerpt<'a> {
        pub text: &'a str,
    }
    
    impl<'a> Excerpt<'a> {
        pub fn new(text: &'a str) -> Self {
            Self { text }
        }
    
        // Lifetime elision applies: returned &str borrows from `self`.
        pub fn first_word(&self) -> &str {
            self.text.split_whitespace().next().unwrap_or("")
        }
    }
    
    // Approach 4: Functional/recursive — annotated helper
    // Finds the longest string in a slice, returning a reference into it.
    pub fn longest_in<'a>(strs: &[&'a str]) -> Option<&'a str> {
        strs.iter().copied().max_by_key(|s| s.len())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longest_first_wins_on_tie() {
            let a = "hello";
            let b = "world";
            assert_eq!(longest(a, b), "hello");
        }
    
        #[test]
        fn test_longest_picks_longer() {
            let a = "short";
            let b = "much longer string";
            assert_eq!(longest(a, b), "much longer string");
        }
    
        #[test]
        fn test_trim_prefix_removes_prefix() {
            assert_eq!(trim_prefix("Hello, Alice!", "Hello, "), "Alice!");
        }
    
        #[test]
        fn test_trim_prefix_no_match_returns_original() {
            assert_eq!(trim_prefix("Hello, Alice!", "Bye, "), "Hello, Alice!");
        }
    
        #[test]
        fn test_excerpt_first_word() {
            let novel = String::from("Call me Ishmael. Some years ago...");
            let excerpt = Excerpt::new(&novel);
            assert_eq!(excerpt.first_word(), "Call");
        }
    
        #[test]
        fn test_excerpt_single_word() {
            let word = String::from("Rust");
            let excerpt = Excerpt::new(&word);
            assert_eq!(excerpt.first_word(), "Rust");
        }
    
        #[test]
        fn test_longest_in_finds_max() {
            let strs = vec!["cat", "elephant", "ox"];
            assert_eq!(longest_in(&strs), Some("elephant"));
        }
    
        #[test]
        fn test_longest_in_empty_returns_none() {
            let strs: Vec<&str> = vec![];
            assert_eq!(longest_in(&strs), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_longest_first_wins_on_tie() {
            let a = "hello";
            let b = "world";
            assert_eq!(longest(a, b), "hello");
        }
    
        #[test]
        fn test_longest_picks_longer() {
            let a = "short";
            let b = "much longer string";
            assert_eq!(longest(a, b), "much longer string");
        }
    
        #[test]
        fn test_trim_prefix_removes_prefix() {
            assert_eq!(trim_prefix("Hello, Alice!", "Hello, "), "Alice!");
        }
    
        #[test]
        fn test_trim_prefix_no_match_returns_original() {
            assert_eq!(trim_prefix("Hello, Alice!", "Bye, "), "Hello, Alice!");
        }
    
        #[test]
        fn test_excerpt_first_word() {
            let novel = String::from("Call me Ishmael. Some years ago...");
            let excerpt = Excerpt::new(&novel);
            assert_eq!(excerpt.first_word(), "Call");
        }
    
        #[test]
        fn test_excerpt_single_word() {
            let word = String::from("Rust");
            let excerpt = Excerpt::new(&word);
            assert_eq!(excerpt.first_word(), "Rust");
        }
    
        #[test]
        fn test_longest_in_finds_max() {
            let strs = vec!["cat", "elephant", "ox"];
            assert_eq!(longest_in(&strs), Some("elephant"));
        }
    
        #[test]
        fn test_longest_in_empty_returns_none() {
            let strs: Vec<&str> = vec![];
            assert_eq!(longest_in(&strs), None);
        }
    }

    Deep Comparison

    OCaml vs Rust: Lifetime Basics

    Side-by-Side Code

    OCaml

    (* OCaml's GC ensures values live as long as they're referenced.
       No annotations needed — the runtime tracks everything. *)
    
    let longest a b =
      if String.length a >= String.length b then a else b
    
    let () =
      let result = longest "long string" "short" in
      assert (result = "long string");
      print_endline "ok"
    

    Rust (idiomatic — explicit lifetime annotation)

    // 'a names the overlap of both input lifetimes.
    // The returned reference cannot outlive either argument.
    pub fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
        if a.len() >= b.len() { a } else { b }
    }
    

    Rust (struct holding a reference)

    pub struct Excerpt<'a> {
        pub text: &'a str,
    }
    
    impl<'a> Excerpt<'a> {
        pub fn first_word(&self) -> &str {
            self.text.split_whitespace().next().unwrap_or("")
        }
    }
    

    Rust (functional — longest in a slice)

    pub fn longest_in<'a>(strs: &[&'a str]) -> Option<&'a str> {
        strs.iter().copied().max_by_key(|s| s.len())
    }
    

    Type Signatures

    ConceptOCamlRust
    String comparison functionval longest : string -> string -> stringfn longest<'a>(a: &'a str, b: &'a str) -> &'a str
    Owned vs borrowedAlways GC-managedString (owned) vs &str (borrowed)
    Struct with referenceAny field, GC handles itRequires 'a on struct and impl block
    Optional referencestring optionOption<&'a str>

    Key Insights

  • OCaml never needs annotations — the garbage collector tracks all live references at runtime, making dangling pointers impossible without any programmer effort.
  • Rust does it at compile time — lifetime annotations ('a) are not runtime metadata; they are hints to the borrow checker that are erased before codegen. Zero overhead.
  • Lifetime elision hides the noise — most single-input functions (fn first_word(&self) -> &str) don't require explicit annotations; the compiler infers them via three elision rules.
  • Structs that hold references must be annotatedExcerpt<'a> tells the compiler "this struct cannot outlive the string slice it borrows," preventing use-after-free at the call site.
  • The annotation describes relationships, not durations'a on longest doesn't say "live for exactly this long"; it says "the output reference comes from one of the inputs," letting the caller reason about scope.
  • When to Use Each Style

    Use explicit lifetime annotations when: a function takes multiple reference parameters and returns a reference — the compiler cannot infer which input the output borrows from.

    Use lifetime elision (no annotation) when: a function takes exactly one reference parameter and returns a reference from it, or the function is a &self method returning a reference (the compiler handles these automatically).

    Exercises

  • Write first_word<'a>(s: &'a str) -> &'a str that returns the first whitespace-delimited word as a borrowed slice.
  • Implement a Cache<'a, T> struct that holds a reference to a slice and a computed value, where both must have the same lifetime.
  • Write longer_name<'a, 'b>(first: &'a str, last: &'b str) -> &'a str and explain why the return lifetime is 'a and not 'b.
  • Open Source Repos