Skip to Content
Course content

18: Lifetime Elision Rules

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you've probably spent some time wrestling with lifetime annotations. I'll be honest: writing &'a str and 'a everywhere feels like you're doing the compiler's job for it. It's tedious, and it clutters the logic of your functions. But if you've noticed, you aren't actually writing those tick marks on every single function. That's because of Lifetime Elision.

Think of lifetime elision like a "shorthand" in a conversation between two old friends. If I'm standing in my kitchen and I say, "Hand me that towel," I don't have to say, "Hand me the towel that is currently hanging on the rack in this specific room for the duration of this cleaning task." You already know which towel I mean because the context makes it obvious. We've both agreed on a set of unspoken rules about how we communicate in the kitchen to save time.

Rust does the exact same thing. The compiler has a set of built-in "social rules" for references. If your function follows these patterns, Rust assumes the lifetimes and fills them in behind the scenes so your code stays clean. Here is how those rules actually map to your code.

The Automatic Assignment of Input Lifetimes

The first rule is the simplest: every input reference gets its own lifetime. If you have a function that takes two references, Rust doesn't assume they are the same. It internally treats them as if they have distinct lifetimes.

// What you write:
fn print_pair(a: &str, b: &str) { ... }

// What Rust sees:
fn print_pair<'a, 'b>(a: &'a str, b: &'b str) { ... }

I like to think of this as Rust being cautious. It doesn't want to assume that a and b are tied to the same source unless you explicitly tell it they are.

The Single Input Shortcut

This is where the real magic happens. If a function has exactly one input lifetime, Rust assumes that this lifetime is the "source of truth" for everything the function returns. This is the most common pattern in Rust.

// You write this:
fn first_word(s: &str) -> &str {
    // ... some logic to find the first word ...
    &s[0..5] 
}

// Rust interprets it as:
fn first_word<'a>(s: &'a str) -> &'a str {
    &s[0..5]
}

Because there's only one reference coming in, it's logically obvious that any reference going out must be tied to that input. Why force you to type 'a twice when there's no other possibility?

The 'self' Exception for Methods

When you're writing methods in an impl block, there's a special rule for &self or &mut self. The lifetime of the object itself is automatically assigned to all output references.

struct Book { title: String }

impl Book {
    // Rust knows the returned &str is tied to the lifetime of 'self'
    fn get_title(&self) -> &str {
        &self.title
    }
}

In this case, Rust thinks: "If you're borrowing a piece of the book, that piece can't live longer than the book itself." It's a common-sense rule that removes a massive amount of boilerplate from your structs.

When the Shorthand Breaks

Eventually, you'll hit a wall where the compiler complains that "missing lifetime specifier" is an error. This usually happens when you have multiple input references and you're returning one of them. Rust doesn't know which input the output is tied to.

// This will FAIL to compile
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

In the example above, the compiler is stuck. Is the return value tied to x? To y? To both? Since it can't guess, it stops being "the friend in the kitchen" and starts being a strict librarian, demanding you explicitly annotate the lifetimes. That's when you have to go back to &'a str.




📋 Practical Task

Refactoring the User Profile Preference Resolver

You are building a system that resolves user preferences. You have a function that takes a "default" preference string and a "user-defined" preference string. The goal is to return the user-defined one if it's not empty; otherwise, return the default.

Currently, the following code is failing to compile because it violates the elision rules (it has two inputs but one output, and the compiler doesn't know which one is being returned).

fn resolve_preference(default: &str, user_pref: &str) -> &str {
    if user_pref.is_empty() {
        default
    } else {
        user_pref
    }
}

fn main() {
    let default_theme = "Light Mode";
    let user_theme = "Dark Mode";
    let final_theme = resolve_preference(default_theme, user_theme);
    println!("The active theme is: {}", final_theme);
}

Your Task: Fix the resolve_preference function by adding the necessary lifetime annotations so the compiler understands that the returned reference will live as long as the shortest of the two input references.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.