899-lifetime-basics — Lifetime Basics
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
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
&self methods); when elision doesn't apply, explicit 'a is required.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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| String comparison function | val longest : string -> string -> string | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str |
| Owned vs borrowed | Always GC-managed | String (owned) vs &str (borrowed) |
| Struct with reference | Any field, GC handles it | Requires 'a on struct and impl block |
| Optional reference | string option | Option<&'a str> |
Key Insights
'a) are not runtime metadata; they are hints to the borrow checker that are erased before codegen. Zero overhead.fn first_word(&self) -> &str) don't require explicit annotations; the compiler infers them via three elision rules.Excerpt<'a> tells the compiler "this struct cannot outlive the string slice it borrows," preventing use-after-free at the call site.'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
first_word<'a>(s: &'a str) -> &'a str that returns the first whitespace-delimited word as a borrowed slice.Cache<'a, T> struct that holds a reference to a slice and a computed value, where both must have the same lifetime.longer_name<'a, 'b>(first: &'a str, last: &'b str) -> &'a str and explain why the return lifetime is 'a and not 'b.