Skip to content

Instantly share code, notes, and snippets.

@abitofhelp
Last active October 11, 2025 01:02
Show Gist options
  • Save abitofhelp/c8adb2f3c296f6392f2dbbd776755672 to your computer and use it in GitHub Desktop.
Save abitofhelp/c8adb2f3c296f6392f2dbbd776755672 to your computer and use it in GitHub Desktop.
Understanding Rust Lifetime Elision - Part 1 of 4

Understanding Rust Lifetime Elision - Part 1:

Rule 1 - Introduction

Part 1 of 4: A comprehensive guide to mastering Rust's lifetime elision rules


Series Navigation

  • Part 1: Introduction and Rule 1 ← You are here
  • Part 2: Rule 2 - The Single Input Lifetime Rule
  • Part 3: Rule 3 - The Method Receiver Rule
  • Part 4: Structs, Lifetimes, and When Elision Doesn't Apply

Introduction

If you've been learning Rust, you've likely encountered lifetime annotations—those mysterious 'a and 'b markers that appear in function signatures. While lifetimes are fundamental to Rust's memory safety guarantees, you don't always need to write them explicitly. This is thanks to lifetime elision rules.

Lifetime elision allows the Rust compiler to infer lifetime annotations in common patterns, making your code cleaner and more readable. However, understanding when and how these rules apply is crucial for mastering Rust's ownership system.

What You'll Learn in This Series

This four-part series will teach you:

  • The three lifetime elision rules in depth
  • When elision applies (and when it doesn't)
  • How to recognize which rule applies in any given situation
  • The difference between function parameters and struct fields

Each rule is illustrated with examples at three complexity levels: simple, intermediate, and complex.

Key Principle

Lifetime elision rules ONLY apply to function and method signatures (parameters and return types). Struct field lifetimes must ALWAYS be explicitly declared and do not participate in elision.

Let's dive into the first rule!


Rule 1: Input Lifetime Rule

Each function/method parameter that is a reference gets its own lifetime parameter.

This is the foundation of lifetime elision. When you write a function with reference parameters, the compiler automatically assigns each reference its own distinct lifetime, even though you don't write it explicitly.

Simple Example

Let's start with the most basic case—a single reference parameter:

/// Prints a reference to an integer
///
/// # Parameters
/// * `x` - Reference to an i32 value
fn print_ref(x: &i32) {
    println!("{}", x);
}

/// Prints a reference to an integer (explicit lifetimes)
///
/// # Parameters
/// * `x` - Reference to an i32 value with lifetime 'a
fn print_ref_explicit<'a>(x: &'a i32) {
    println!("{}", x);
}

What's happening: The compiler sees the reference parameter &i32 and automatically assigns it a lifetime. Both versions are equivalent—the elided version is just syntactic sugar.

Intermediate Example

Now let's look at multiple reference parameters:

/// Compares two integer references
///
/// # Parameters
/// * `x` - First integer reference to compare
/// * `y` - Second integer reference to compare
///
/// # Returns
/// * `bool` - True if x is greater than y
fn compare(x: &i32, y: &i32) -> bool {
    x > y
}

/// Compares two integer references (explicit lifetimes)
///
/// # Parameters
/// * `x` - First integer reference with lifetime 'a
/// * `y` - Second integer reference with lifetime 'b
///
/// # Returns
/// * `bool` - True if x is greater than y
fn compare_explicit<'a, 'b>(x: &'a i32, y: &'b i32) -> bool {
    x > y
}

What's happening: Each reference parameter gets its own distinct lifetime. x gets lifetime 'a, and y gets lifetime 'b. The two references don't need to have the same lifetime because we're just comparing their values, not returning any references.

Complex Example

Let's examine a more realistic scenario with multiple reference types:

struct Config {
    setting: bool,
}

/// Processes data with configuration
///
/// # Parameters
/// * `name` - String reference for identification
/// * `values` - Slice reference containing integer values
/// * `config` - Mutable reference to configuration
fn process_data(name: &str, values: &[i32], config: &mut Config) {
    // ... implementation
}

/// Processes data with configuration (explicit lifetimes)
///
/// # Parameters
/// * `name` - String reference with lifetime 'a
/// * `values` - Slice reference with lifetime 'b
/// * `config` - Mutable reference to Config with lifetime 'c
fn process_data_explicit<'a, 'b, 'c>(
    name: &'a str,
    values: &'b [i32],
    config: &'c mut Config
) {
    // ... implementation
}

What's happening: Three reference parameters mean three distinct lifetimes:

  • name: &strname: &'a str
  • values: &[i32]values: &'b [i32]
  • config: &mut Configconfig: &'c mut Config

Notice that mutable references (&mut) follow the same rule as immutable references (&).


Rule 1 with Non-Reference Parameters

A crucial point: Rule 1 only applies to reference parameters. Owned types like i32, String, bool, etc., don't have lifetimes and are completely ignored by elision rules.

Example with Mixed Parameters

/// Calculates data length multiplied by factor
///
/// # Parameters
/// * `factor` - Integer multiplier (owned, no lifetime)
/// * `data` - String reference to measure
///
/// # Returns
/// * `usize` - Length of data multiplied by factor
fn calculate(factor: i32, data: &str) -> usize {
    data.len() * factor as usize
}

/// Calculates data length multiplied by factor (explicit lifetimes)
///
/// # Parameters
/// * `factor` - Integer multiplier (owned, no lifetime)
/// * `data` - String reference with lifetime 'a
///
/// # Returns
/// * `usize` - Length of data multiplied by factor
fn calculate_explicit<'a>(factor: i32, data: &'a str) -> usize {
    data.len() * factor as usize
}

Key insight: Even though we have two parameters, only data is a reference, so only it gets a lifetime. The i32 parameter is owned and doesn't participate in lifetime elision.

More Complex Mixed Parameters

/// Processes text with configuration flags
///
/// # Parameters
/// * `count` - Number of repetitions (owned, no lifetime)
/// * `text` - String reference to process
/// * `enabled` - Boolean flag (owned, no lifetime)
/// * `prefix` - String reference for output prefix
fn process(count: u32, text: &str, enabled: bool, prefix: &str) {
    if enabled {
        println!("{}: {}", prefix, text.repeat(count as usize));
    }
}

/// Processes text with configuration flags (explicit lifetimes)
///
/// # Parameters
/// * `count` - Number of repetitions (owned, no lifetime)
/// * `text` - String reference with lifetime 'a
/// * `enabled` - Boolean flag (owned, no lifetime)
/// * `prefix` - String reference with lifetime 'b
fn process_explicit<'a, 'b>(count: u32, text: &'a str, enabled: bool, prefix: &'b str) {
    if enabled {
        println!("{}: {}", prefix, text.repeat(count as usize));
    }
}

What's happening: Out of four parameters, only two are references (text and prefix), so only those two get lifetimes. The owned parameters (count: u32 and enabled: bool) are ignored.


Key Takeaways for Rule 1

  1. Every reference parameter gets its own lifetime - whether immutable (&) or mutable (&mut)
  2. Owned parameters don't get lifetimes - types like i32, String, bool are ignored
  3. Each reference is independent - different parameters can have different lifetimes
  4. This is just the first step - Rule 1 assigns input lifetimes; other rules handle output lifetimes

What's Next?

In Part 2, we'll explore Rule 2: The Single Input Lifetime Rule, which explains what happens when you have exactly one reference input and need to return a reference. This is one of the most common patterns in Rust!


Practice Exercise

Before moving to Part 2, try to identify which parameters get lifetimes in these function signatures:

fn example1(x: &str, y: usize) -> usize { /* ... */ }

fn example2(a: &i32, b: &i32, c: &i32) -> i32 { /* ... */ }

fn example3(data: Vec<u8>, name: &str) -> String { /* ... */ }
Click to see answers
  • example1: Only x gets a lifetime ('a). y is owned (usize).
  • example2: All three parameters get lifetimes ('a, 'b, 'c). Return type is owned, so no lifetime needed.
  • example3: Only name gets a lifetime ('a). data and the return type are owned.

Series Navigation:


This article is part of a comprehensive educational series on Rust lifetime elision. Stay tuned for Part 2!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment