Skip to content

Instantly share code, notes, and snippets.

@abitofhelp
Last active October 11, 2025 00:59
Show Gist options
  • Save abitofhelp/ed71fa0a840f9082dc2dd14ff68d2665 to your computer and use it in GitHub Desktop.
Save abitofhelp/ed71fa0a840f9082dc2dd14ff68d2665 to your computer and use it in GitHub Desktop.
Understanding Rust Lifetime Elision - Part 2

Understanding Rust Lifetime Elision - Part 2:

Rule 2 - The Single Input Lifetime Rule

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


Series Navigation

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

Recap from Part 1

In Part 1, we learned Rule 1: each reference parameter gets its own lifetime. Now we'll explore what happens when you need to return a reference from a function.


Rule 2: Single Input Lifetime Rule

If there is exactly one input lifetime parameter (one reference parameter), that lifetime is assigned to all output lifetime parameters.

This is arguably the most common elision pattern in Rust. When you take one reference as input and return a reference as output, the compiler assumes the output's lifetime is tied to the input's lifetime.

Why This Rule Exists

When a function takes one reference and returns a reference, it's almost always returning something derived from that input. Rule 2 captures this common pattern, eliminating the need for explicit lifetime annotations.


Simple Example

Let's start with a basic string slice operation:

/// Gets the first character from a string
///
/// # Parameters
/// * `s` - String reference to extract from
///
/// # Returns
/// * `&str` - Reference to the first character
fn get_first_char(s: &str) -> &str {
    &s[0..1]
}

/// Gets the first character from a string (explicit lifetimes)
///
/// # Parameters
/// * `s` - String reference with lifetime 'a
///
/// # Returns
/// * `&str` - String reference with same lifetime 'a
fn get_first_char_explicit<'a>(s: &'a str) -> &'a str {
    &s[0..1]
}

What's happening:

  1. Rule 1 gives s a lifetime (let's call it 'a)
  2. Rule 2 sees there's exactly one input lifetime
  3. The return type &str automatically gets the same lifetime 'a

The returned reference lives as long as the input reference—which makes sense because we're returning a slice of the input!


Simple Example - Returning Struct with References

Rule 2 also works when returning structs that contain references:

/// User session data container
///
/// # Fields
/// * `username` - Reference to username string with lifetime 'a
/// * `user_id` - User ID number (owned, no lifetime)
///
/// Note: Struct fields must always have explicit lifetime annotations.
/// Elision does NOT apply to struct definitions.
struct UserSession<'a> {
    username: &'a str,    // Must explicitly declare lifetime 'a
    user_id: u32,         // Owned field - no lifetime needed
}

/// Creates a new user session
/// Rule 2 applies here: single input reference lifetime flows to output
///
/// # Parameters
/// * `name` - String reference for username
/// * `id` - User ID number (owned, no lifetime)
///
/// # Returns
/// * `UserSession` - Session with username reference having same lifetime as input
fn create_session(name: &str, id: u32) -> UserSession {
    UserSession {
        username: name,
        user_id: id,
    }
}

/// Creates a new user session (explicit lifetimes)
///
/// # Parameters
/// * `name` - String reference with lifetime 'a
/// * `id` - User ID number (owned, no lifetime)
///
/// # Returns
/// * `UserSession<'a>` - Session with username having lifetime 'a
fn create_session_explicit<'a>(name: &'a str, id: u32) -> UserSession<'a> {
    UserSession {
        username: name,
        user_id: id,
    }
}

Important distinction:

  • The struct definition (UserSession<'a>) must have explicit lifetimes
  • The function signature (fn create_session) can use elision for its parameters and return type
  • Rule 2 connects the input lifetime to the output struct's lifetime

Intermediate Example

Rule 2 works with multiple output references in tuples:

/// Splits a string at the first comma
///
/// # Parameters
/// * `s` - String reference to split
///
/// # Returns
/// * `(&str, &str)` - Tuple of (before comma, after comma)
fn split_at_comma(s: &str) -> (&str, &str) {
    match s.find(',') {
        Some(pos) => (&s[..pos], &s[pos+1..]),
        None => (s, ""),
    }
}

/// Splits a string at the first comma (explicit lifetimes)
///
/// # Parameters
/// * `s` - String reference with lifetime 'a
///
/// # Returns
/// * `(&'a str, &'a str)` - Tuple where both parts have lifetime 'a
fn split_at_comma_explicit<'a>(s: &'a str) -> (&'a str, &'a str) {
    match s.find(',') {
        Some(pos) => (&s[..pos], &s[pos+1..]),
        None => (s, ""),
    }
}

What's happening:

  • Single input: s: &str gets lifetime 'a
  • Multiple outputs: both &str in the tuple get lifetime 'a
  • Both returned slices must live as long as the input

Complex Example

Rule 2 applies to nested generic types like Result and Option:

/// Parses header from byte data
///
/// # Parameters
/// * `data` - Byte slice reference to parse
///
/// # Returns
/// * `Result<&[u8], &str>` - Ok with header bytes, or Err with error message
fn parse_header(data: &[u8]) -> Result<&[u8], &str> {
    if data.len() < 4 {
        Err("header too short")
    } else {
        Ok(&data[0..4])
    }
}

/// Parses header from byte data (explicit lifetimes)
///
/// # Parameters
/// * `data` - Byte slice reference with lifetime 'a
///
/// # Returns
/// * `Result<&'a [u8], &'a str>` - Both Ok and Err variants have lifetime 'a
fn parse_header_explicit<'a>(data: &'a [u8]) -> Result<&'a [u8], &'a str> {
    if data.len() < 4 {
        Err("header too short")
    } else {
        Ok(&data[0..4])
    }
}

Key insight: Both the &[u8] in the Ok variant and the &str in the Err variant get the same lifetime from the input. The compiler applies Rule 2 recursively through the Result type.

Another Complex Example with Option

/// Extracts inner value from Option wrapper
///
/// # Parameters
/// * `wrapper` - Reference to Option containing value
///
/// # Returns
/// * `Option<&T>` - Some with reference to inner value, or None
fn extract_field<T>(wrapper: &Option<T>) -> Option<&T> {
    wrapper.as_ref()
}

/// Extracts inner value from Option wrapper (explicit lifetimes)
///
/// # Parameters
/// * `wrapper` - Reference to Option with lifetime 'a
///
/// # Returns
/// * `Option<&'a T>` - Option containing reference with lifetime 'a
fn extract_field_explicit<'a, T>(wrapper: &'a Option<T>) -> Option<&'a T> {
    wrapper.as_ref()
}

What's happening: The &T inside Option<&T> gets the same lifetime as the input &Option<T>.


Rule 2 with Non-Reference Parameters

Remember: owned parameters don't count as "input lifetimes." Rule 2 only triggers when there's exactly one reference parameter, regardless of how many owned parameters exist.

Example: Single Reference + Owned Parameters

/// Formats data string with repetition count
///
/// # Parameters
/// * `data` - String reference to format
/// * `times` - Number of times to repeat (owned, no lifetime)
///
/// # Returns
/// * `&str` - Reference to the data string
fn format_with_count(data: &str, times: usize) -> &str {
    data
}

/// Formats data string with repetition count (explicit lifetimes)
///
/// # Parameters
/// * `data` - String reference with lifetime 'a
/// * `times` - Number of times to repeat (owned, no lifetime)
///
/// # Returns
/// * `&'a str` - Reference to data with same lifetime 'a
fn format_with_count_explicit<'a>(data: &'a str, times: usize) -> &'a str {
    data
}

Why Rule 2 applies: Even though there are two parameters, only data is a reference. The times: usize is owned and doesn't count.

More Complex Example with Multiple Owned Parameters

/// Extracts a segment from a byte buffer
///
/// # Parameters
/// * `buffer` - Byte slice reference to extract from
/// * `offset` - Starting position (owned, no lifetime)
/// * `length` - Number of bytes to extract (owned, no lifetime)
///
/// # Returns
/// * `&[u8]` - Reference to the extracted byte segment
fn extract_segment(buffer: &[u8], offset: usize, length: usize) -> &[u8] {
    &buffer[offset..offset + length]
}

/// Extracts a segment from a byte buffer (explicit lifetimes)
///
/// # Parameters
/// * `buffer` - Byte slice reference with lifetime 'a
/// * `offset` - Starting position (owned, no lifetime)
/// * `length` - Number of bytes to extract (owned, no lifetime)
///
/// # Returns
/// * `&'a [u8]` - Reference to segment with same lifetime 'a
fn extract_segment_explicit<'a>(
    buffer: &'a [u8],
    offset: usize,
    length: usize
) -> &'a [u8] {
    &buffer[offset..offset + length]
}

Key point: Three parameters total, but only one reference (buffer). Rule 2 still applies!

Final Example

/// Repeats a character operation on source string
///
/// # Parameters
/// * `ch` - Character to use (owned, no lifetime)
/// * `source` - String reference to process
/// * `times` - Number of repetitions (owned, no lifetime)
///
/// # Returns
/// * `&str` - Reference to the source string
fn repeat_char(ch: char, source: &str, times: i32) -> &str {
    source
}

/// Repeats a character operation on source string (explicit lifetimes)
///
/// # Parameters
/// * `ch` - Character to use (owned, no lifetime)
/// * `source` - String reference with lifetime 'a
/// * `times` - Number of repetitions (owned, no lifetime)
///
/// # Returns
/// * `&'a str` - Reference to source with same lifetime 'a
fn repeat_char_explicit<'a>(ch: char, source: &'a str, times: i32) -> &'a str {
    source
}

When Rule 2 Does NOT Apply

Rule 2 only works when there's exactly one reference input. If you have:

  • Zero reference inputs → No lifetime to assign to output
  • Two or more reference inputs → Ambiguous which lifetime the output should have

Example where Rule 2 doesn't apply:

// This will NOT compile without explicit lifetimes
fn choose(first: &str, second: &str, use_first: bool) -> &str {
    if use_first { first } else { second }
}

// Compiler error: can't determine output lifetime
// Is it tied to 'first' or 'second'?

You'd need to write explicit lifetimes:

fn choose<'a>(first: &'a str, second: &'a str, use_first: bool) -> &'a str {
    if use_first { first } else { second }
}

Key Takeaways for Rule 2

  1. Exactly one reference input is required for Rule 2 to apply
  2. Owned parameters don't count - they're ignored when counting reference inputs
  3. All output references get the same lifetime as the single input
  4. Works with nested types like Result<&T, &E>, Option<&T>, tuples, etc.
  5. Structs must still declare lifetimes explicitly in their definitions

What's Next?

In Part 3, we'll explore Rule 3: The Method Receiver Rule, which handles the common case of methods that take &self along with other parameters. This is crucial for working with structs and understanding object-oriented patterns in Rust!


Practice Exercise

Which of these functions can use Rule 2 for lifetime elision?

fn a(x: &str) -> &str { x }

fn b(x: &str, y: &str) -> &str { x }

fn c(x: &str, count: usize) -> &str { x }

fn d(x: String) -> &str { &x }
Click to see answers
  • a: ✅ Rule 2 applies - one reference input, reference output
  • b: ❌ Rule 2 doesn't apply - two reference inputs (ambiguous)
  • c: ✅ Rule 2 applies - only one reference input (count is owned)
  • d: ❌ Won't compile - no reference inputs, but tries to return a reference (would be dangling)

Series Navigation:


This article is part of a comprehensive educational series on Rust lifetime elision. Continue to Part 3 to learn about methods and &self!

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