Part 2 of 4: A comprehensive guide to mastering Rust's lifetime elision rules
- 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
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.
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.
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.
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:
- Rule 1 gives
sa lifetime (let's call it'a) - Rule 2 sees there's exactly one input lifetime
- The return type
&strautomatically 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!
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
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: &strgets lifetime'a - Multiple outputs: both
&strin the tuple get lifetime'a - Both returned slices must live as long as the input
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.
/// 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>.
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.
/// 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.
/// 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!
/// 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
}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 }
}- Exactly one reference input is required for Rule 2 to apply
- Owned parameters don't count - they're ignored when counting reference inputs
- All output references get the same lifetime as the single input
- Works with nested types like
Result<&T, &E>,Option<&T>, tuples, etc. - Structs must still declare lifetimes explicitly in their definitions
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!
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 (
countis owned) - d: ❌ Won't compile - no reference inputs, but tries to return a reference (would be dangling)
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
This article is part of a comprehensive educational series on Rust lifetime elision. Continue to Part 3 to learn about methods and &self!