Skip to content

Instantly share code, notes, and snippets.

@abitofhelp
Last active October 11, 2025 01:01
Show Gist options
  • Select an option

  • Save abitofhelp/e8ae5bf1a21fba416bbe005264621488 to your computer and use it in GitHub Desktop.

Select an option

Save abitofhelp/e8ae5bf1a21fba416bbe005264621488 to your computer and use it in GitHub Desktop.

Revisions

  1. abitofhelp revised this gist Oct 11, 2025. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions series-part-4-structs-and-when-elision-fails.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    # Understanding Rust Lifetime Elision - Part 4:
    # Structs, Lifetimes, and When Elision Doesn't Apply

    **Part 4 of 4: A comprehensive guide to mastering Rust's lifetime elision rules**
  2. abitofhelp created this gist Oct 11, 2025.
    493 changes: 493 additions & 0 deletions series-part-4-structs-and-when-elision-fails.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,493 @@
    # Structs, Lifetimes, and When Elision Doesn't Apply

    **Part 4 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](#)
    - [Part 3: Rule 3 - The Method Receiver Rule](#)
    - **Part 4: Structs, Lifetimes, and When Elision Doesn't Apply** ← You are here

    ---

    ## Recap: The Three Elision Rules

    Before we explore the limits of lifetime elision, let's review what we've learned:

    1. **Rule 1**: Each reference parameter gets its own lifetime
    2. **Rule 2**: If there's exactly one reference input, all outputs get that lifetime
    3. **Rule 3**: In methods, return values get `&self`'s lifetime (not other parameters')

    These rules apply **ONLY to function and method signatures**. Now let's explore where elision does NOT work.

    ---

    ## Part 1: Struct Fields Require Explicit Lifetimes

    **The most important principle:** Struct field lifetimes must ALWAYS be explicitly declared. There is NO lifetime elision for struct definitions.

    ### Why No Elision for Structs?

    Structs are type definitions, not function signatures. They declare what lifetimes they need without any context about where those lifetimes come from. The compiler needs you to be explicit about these relationships.

    ---

    ## Simple Struct Example - Single Lifetime

    ```rust
    /// User session data container
    ///
    /// # Fields
    /// * `username` - Reference to username string with lifetime 'a
    /// * `user_id` - User ID number (owned, no lifetime)
    ///
    /// Lifetime 'a MUST be explicitly declared - no elision for struct fields.
    struct UserSession<'a> {
    username: &'a str, // Explicit lifetime required
    user_id: u32, // No lifetime needed (owned)
    }
    ```

    **Key points:**

    - The `<'a>` after `UserSession` is required
    - Each reference field must use a declared lifetime
    - Owned fields (like `u32`) don't need lifetimes
    - This is a **declaration**, not elision-eligible code

    ### Using the Struct in Functions

    Even though the struct definition requires explicit lifetimes, **functions that use the struct can still benefit from elision**:

    ```rust
    // Elision works here (Rule 2 applies)
    fn create_session(name: &str, id: u32) -> UserSession {
    UserSession {
    username: name,
    user_id: id,
    }
    }

    // What the compiler sees
    fn create_session_explicit<'a>(name: &'a str, id: u32) -> UserSession<'a> {
    UserSession {
    username: name,
    user_id: id,
    }
    }
    ```

    **Important distinction:**

    - Struct **definition** (`struct UserSession<'a>`) → explicit lifetimes required
    - Function **using** the struct (`fn create_session`) → elision can apply

    ---

    ## Intermediate Struct Example - Multiple Lifetimes

    Structs can have multiple independent lifetimes:

    ```rust
    /// HTTP request data container
    ///
    /// # Fields
    /// * `method` - Reference to HTTP method string with lifetime 'a
    /// * `path` - Reference to request path string with lifetime 'b
    /// * `status_code` - HTTP status code (owned, no lifetime)
    /// * `content_length` - Content length in bytes (owned, no lifetime)
    ///
    /// Both lifetimes 'a and 'b MUST be explicitly declared.
    struct HttpRequest<'a, 'b> {
    method: &'a str, // Explicit lifetime 'a required
    path: &'b str, // Explicit lifetime 'b required
    status_code: u16, // No lifetime needed
    content_length: usize, // No lifetime needed
    }
    ```

    **Why multiple lifetimes?** The `method` and `path` might come from different sources with different lifespans. By using separate lifetimes, we're telling the compiler they're independent.

    ### Using Multiple Lifetimes in Functions

    ```rust
    // Without elision (explicit)
    fn parse_request<'a, 'b>(
    method_str: &'a str,
    path_str: &'b str,
    code: u16
    ) -> HttpRequest<'a, 'b> {
    HttpRequest {
    method: method_str,
    path: path_str,
    status_code: code,
    content_length: 0,
    }
    }
    ```

    **Note:** This function **cannot use elision** because:

    - It has two reference parameters (Rule 2 doesn't apply)
    - It's not a method (Rule 3 doesn't apply)
    - We must be explicit about which lifetime goes where

    ---

    ## Complex Struct Example - Nested Structs

    Structs can contain other structs with lifetimes:

    ```rust
    /// Network packet header
    ///
    /// # Fields
    /// * `protocol` - Reference to protocol name with lifetime 'a
    /// * `version` - Protocol version (owned, no lifetime)
    ///
    /// Lifetime 'a MUST be explicitly declared.
    struct PacketHeader<'a> {
    protocol: &'a str, // Explicit lifetime required
    version: u8, // No lifetime needed
    }

    /// Network packet with header and payload
    ///
    /// # Fields
    /// * `header` - PacketHeader with lifetime 'a
    /// * `payload` - Reference to payload bytes with lifetime 'b
    /// * `sequence_num` - Packet sequence number (owned, no lifetime)
    /// * `timestamp` - Unix timestamp (owned, no lifetime)
    ///
    /// Both lifetimes 'a and 'b MUST be explicitly declared.
    struct NetworkPacket<'a, 'b> {
    header: PacketHeader<'a>, // Explicit lifetime 'a required
    payload: &'b [u8], // Explicit lifetime 'b required
    sequence_num: u64, // No lifetime needed
    timestamp: i64, // No lifetime needed
    }
    ```

    **What's happening:**

    - `PacketHeader` has its own lifetime `'a`
    - `NetworkPacket` also needs lifetime `'a` for its `header` field
    - `NetworkPacket` has an additional lifetime `'b` for its `payload` field
    - This creates a hierarchy of lifetimes that must be explicitly tracked

    ### Creating Nested Structs

    ```rust
    // Must be explicit - multiple references, not a method
    fn create_packet<'a, 'b>(
    protocol_name: &'a str,
    ver: u8,
    data: &'b [u8],
    seq: u64
    ) -> NetworkPacket<'a, 'b> {
    let header = PacketHeader {
    protocol: protocol_name,
    version: ver,
    };

    NetworkPacket {
    header,
    payload: data,
    sequence_num: seq,
    timestamp: 0,
    }
    }
    ```

    ---

    ## Part 2: When Elision Doesn't Apply to Functions

    Even for functions, there are cases where you must write explicit lifetimes.

    ### Case 1: Multiple Reference Inputs, Returning One of Them

    ```rust
    // 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 which input lifetime the output should have

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

    **Why elision fails:** Two reference inputs, and we could return either one. The compiler can't guess which lifetime applies.

    ### Case 2: Returning References Not Tied to Inputs

    ```rust
    // This is a compile error - can't return reference to local
    // fn create_string() -> &str {
    // let s = String::from("hello");
    // &s // ERROR: `s` doesn't live long enough
    // }

    // This works - returning a reference to static data
    fn get_constant() -> &'static str {
    "hello"
    }
    ```

    **Key insight:** If you're not returning a reference tied to an input parameter, you typically need `'static` or another explicit lifetime.

    ### Case 3: Complex Lifetime Relationships

    ```rust
    struct Parser<'a> {
    text: &'a str,
    }

    impl<'a> Parser<'a> {
    // Need explicit lifetimes - we want to return a reference to
    // the parameter, not self
    fn first_or<'b>(&'b self, default: &'b str) -> &'b str
    where
    'a: 'b // 'a must outlive 'b
    {
    if self.text.is_empty() {
    default
    } else {
    self.text
    }
    }
    }
    ```

    **Advanced pattern:** Using lifetime bounds (`'a: 'b` means `'a` outlives `'b`) to express complex relationships.

    ### Case 4: Associated Types and Trait Implementations

    ```rust
    trait Extractor {
    // Need explicit lifetime in trait
    fn extract<'a>(&'a self, data: &'a str) -> &'a str;
    }

    struct SimpleExtractor;

    impl Extractor for SimpleExtractor {
    fn extract<'a>(&'a self, data: &'a str) -> &'a str {
    data
    }
    }
    ```

    **Trait signatures** often require explicit lifetimes because traits define contracts that must work across multiple implementations.

    ---

    ## Part 3: Common Patterns and Best Practices

    ### Pattern 1: Single Lifetime for Related Fields

    When all reference fields in a struct come from the same source:

    ```rust
    struct Document<'a> {
    title: &'a str,
    body: &'a str,
    author: &'a str,
    }

    // All fields share lifetime 'a - they come from the same source
    ```

    ### Pattern 2: Multiple Lifetimes for Independence

    When references come from different sources:

    ```rust
    struct Config<'a, 'b> {
    app_name: &'a str, // From command-line args
    file_path: &'b str, // From config file
    }

    // Different lifetimes allow independent sources
    ```

    ### Pattern 3: Owned + Borrowed Hybrid

    Combine owned and borrowed data:

    ```rust
    struct UserProfile<'a> {
    username: String, // Owned - we control this
    bio: &'a str, // Borrowed - references external data
    follower_count: u32, // Owned primitive
    }
    ```

    ---

    ## Part 4: Debugging Lifetime Errors

    ### Understanding Compiler Messages

    When elision fails, the compiler tells you:

    ```rust
    fn broken(x: &str, y: &str) -> &str {
    x
    }

    // Error message:
    // this function's return type contains a borrowed value,
    // but the signature does not say whether it is borrowed from `x` or `y`
    ```

    **Fix:** Add explicit lifetimes to clarify:

    ```rust
    fn fixed<'a>(x: &'a str, y: &str) -> &'a str {
    x
    }
    ```

    ### Common Mistake: Returning References to Locals

    ```rust
    // WRONG - returns reference to local variable
    // fn create() -> &str {
    // let s = String::from("hello");
    // &s // ERROR: s is dropped at end of function
    // }

    // RIGHT - return owned data
    fn create() -> String {
    String::from("hello")
    }

    // OR - return 'static reference
    fn create_static() -> &'static str {
    "hello" // String literals have 'static lifetime
    }
    ```

    ---

    ## Summary: When to Use Explicit Lifetimes

    | Scenario | Elision Works? | Notes |
    | ---------------------------------------- | -------------- | ------------------------ |
    | Struct field declarations | ❌ Never | Always explicit |
    | Single `&` parameter → `&` return | ✅ Rule 2 | Common pattern |
    | Multiple `&` parameters → no return refs | ✅ Rule 1 | Just inputs |
    | Method with `&self``&` return | ✅ Rule 3 | Returns `self` data |
    | Multiple `&` params → `&` return | ❌ Usually not | Ambiguous source |
    | Returning ref to parameter (not `self`) | ❌ No | Must be explicit |
    | Complex lifetime relationships | ❌ No | Use bounds like `'a: 'b` |
    | Trait definitions | ⚠️ Sometimes | Often need explicit |

    ---

    ## Final Key Takeaways

    1. **Struct definitions ALWAYS require explicit lifetimes** for reference fields
    2. **Functions using structs can still benefit from elision** (for their signatures)
    3. **Elision is syntactic sugar** - lifetimes are always there, just sometimes inferred
    4. **When in doubt, be explicit** - clarity beats brevity in complex cases
    5. **The compiler is your friend** - error messages guide you to solutions

    ---

    ## Conclusion

    You've now completed your journey through Rust's lifetime elision rules! You understand:

    -**Rule 1**: Each reference parameter gets its own lifetime
    -**Rule 2**: Single reference input flows to all outputs
    -**Rule 3**: Methods return `&self`'s lifetime
    -**Struct lifetimes**: Always explicit, no elision
    -**When elision fails**: And how to fix it

    ### What's Next?

    Now that you understand lifetime elision, you're ready to:

    - Write idiomatic Rust code with minimal lifetime annotations
    - Understand compiler error messages about lifetimes
    - Design structs and APIs with appropriate lifetime parameters
    - Recognize when to be explicit for clarity

    ### Further Reading

    - [The Rust Book - Chapter 10.3: Validating References with Lifetimes](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html)
    - [Rust RFC 141: Lifetime Elision](https://rust-lang.github.io/rfcs/0141-lifetime-elision.html)
    - [Rustonomicon - Advanced Lifetimes](https://doc.rust-lang.org/nomicon/lifetimes.html)

    ---

    ## Final Practice Challenge

    Identify what's wrong and fix these examples:

    ```rust
    // 1. Does this compile?
    struct Wrapper {
    data: &str,
    }

    // 2. Does this compile?
    fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
    }

    // 3. Does this compile?
    fn first<'a>(x: &'a str, y: &str) -> &'a str {
    x
    }
    ```

    <details>
    <summary>Click to see answers</summary>


    **1. No** - Struct needs explicit lifetime:

    ```rust
    struct Wrapper<'a> {
    data: &'a str,
    }
    ```

    **2. No** - Function needs explicit lifetime (two inputs, ambiguous output):

    ```rust
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
    }
    ```

    **3. Yes!** - This compiles. We explicitly declared that output has same lifetime as `x`. The `y` parameter can have a different lifetime since we're not returning it.

    </details>

    ---

    **Series Navigation:**

    - [Part 1: Introduction and Rule 1](#)
    - [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** ← You are here

    ---

    *Thank you for reading this comprehensive series on Rust lifetime elision! I hope these articles help you write better, more idiomatic Rust code. Happy coding!*

    ---

    **Author's Note:** This series was created for educational purposes to help developers understand Rust's lifetime elision rules through comprehensive examples at multiple complexity levels. Feel free to share and use these materials to help others learn Rust!