Why do I need to add a lifetime specifier when the field in the struct type definition is a reference?

If it's so that the lifetime of an instance of the struct type doesn't exceed the lifetime of the data referenced by the field, the compiler could have inferred that, just like the guidance given in the error report (add <'a>), so why do I need to manually mark it?

fn main() {
    let user1 = User {
        email: "fdsa",
        username: "fds",
        active: true,
        sign_in_count: 556,
    };
}

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

The following error was reported: 
error[E0106]: missing lifetime specifier
  --> src\main.rs:11:15
   |
11 |     username: &str,
   |               ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
10 ~ struct User<'a> {
11 ~     username: &'a str,
   |

error[E0106]: missing lifetime specifier
  --> src\main.rs:12:12
   |
12 |     email: &str,
   |            ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
10 ~ struct User<'a> {
11 |     username: &str,
12 ~     email: &'a str,
   |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `rust_rover_test` (bin "rust_rover_test") due to 2 previous errors
  • Knowing a type is parameterized by a lifetime is crucial information that should not be less visible
  • It keeps the definition in sync with turbofish and other annotations
  • There's no good approach to elision when there are multiple lifetimes in the struct
    • e.g. under a naive approach, reordering the fields of User would become a breaking change
3 Likes

While these things are important… I suspect more important is the next observation:

  • 99% of time complicated data structure with reference (let alone multiple references) means that code is written by a novice and would be thrown away soon – and replaced with something else when said novice would find out why that doesn't suit the needs of his (or her) program.

That's why there are no need to invent clever rules: since the code that you are writing wouldn't survive for long and wouldn't be used, anyway… there are no need to optimize it.

P.S. Rust “reference” notion comes from C++, and thus they are radically different from “references” in most languages with tracing GC. Instead of using them to pass things around “by reference” you used them for a temporary loans. Which probably means that your don't want structure User as it's defined in your program at all. And when you would use something else for email and username (most likely String, but could be Box<str> or Arc<Mutex<str>>) problem would automatically go away since you wouldn't have references, wouldn't have lifetimes and wouldn't need to do anything to work with them.

1 Like

Uh, I'm learning about rust and I'm wondering why the lifetime specifier needs to be added and what information does it provide to the compiler? The code is only for additional clarification of the issue, not the actual project.

1 Like

Thanks, maybe the replies are too abstract for newbies, I still don't understand why I need to manually add the lifetime specifier. Question 1: Why is the lifetime important to the compiler? What information does it provide to the compiler? Question 2 : Why do you need multiple lifetimes? What is the difference between it and a single lifetime?

Rust lifetimes -- those '_ things -- generally represent the duration of a borrow, created when you create a reference. They're a type level property, and don't directly correspond to the liveness of values.

A struct may contain distinct lifetimes if the fields contain more than one borrow. Sometimes variance makes it possibile to use the same lifetime for different borrows, but not always. Lifetimes can also show up in dyn types, which can be another reason for multiple lifetimes.

Lifetimes are how the compiler analyzes what places (variables, fields, dereferences) are borrowed, and for how long (control flow wise), at complie time. The analysis is a vital part of Rust's memory safety guarantees.

The part of the compiler that does the analysis is called the borrow checker. You'll encounter borrow check errors as you learn Rust, and probably come to appreciate how the presence of lifetimes is crucial information for developers.

1 Like

Can you give an example of a struct definition that is suitable for multiple lifetimes? Thanks a lot!

Here's an example.

Note that while Rust lifetime rules are conceptually simple they also include bazillion extensions and special warts that make more programs compileable. That's because developers begged for them: original Rust, Rust 1.0 had very simple borrow-checked and it was easy to understand how it works… but using it was very hard. Today it's the opposite: there are lots of leniency so using it is easy… but fully understand it is hard.

That's in my example I haven't used simple literals but used String::from. Because literal like "fdsa" or "fds" doesn't carry any lifetime information at all[1]!

That's why I take literal, convert it to string then immediately put result back into &str:

    let email1: &str = &String::from("foo@bar.baz");
    let email2: &str = &String::from("hello@example.com");
…
    let alice: &str = &String::from("Alice");
    let bob: &str = &String::from("Bob");

This seemingly useless operation is core to understanding: I still have reference to the string with exact same content as before, but now that reference couldn't exist forever: it points of temporary String – and that String is destroyed at the closing brace, at the end of scope.

And I also create these variables in difference places: user1 and user2 come as arguments into foo function and they leave that function – that way Rust couldn't “help me”: references to user have to outlive the foo function, while references email1 and email2 point to the local objects that don't outlive the function – and thus couldn't live as long as user1 and user2. Temporary strings would be gone after return from the function, there are no way to make references live longer.

And now you see why Users is defined like this:

struct Users<'a, 'b> {
    user1: &'a str,
    email1: &'b str,
    user2: &'a str,
    email2: &'b str,
}

We have four references but only two lifetimes. Why? Because we want to select them:

fn select_use_and_email<'a, 'b>(
    Users { user1, email1, user2, email2 } : Users<'a, 'b>,
    use_first: bool) -> (&'a str, &'b str) {
    (
        if use_first { user1 } else { user2 },
        if use_first { email1 } else { email2 },
    )
}

Here I would return either user1 or user2 and also email1 or email2. Is it possible to do this with four references? Sure, like this:

fn select_use_and_email<'a, 'b, 'c: 'a, 'd: 'b>(
    Users { user1, email1, user2, email2 } : Users<'a, 'b, 'c, 'd>,
    use_first: bool) -> (&'a str, &'b str) {
    (
        if use_first { user1 } else { user2 },
        if use_first { email1 } else { email2 },
    )
}

struct Users<'a, 'b, 'c, 'd> {
    user1: &'a str,
    email1: &'b str,
    user2: &'c str,
    email2: &'d str,
}

Here I needed to tell the compiler that 'c outlives 'a and 'd outlives 'b. Only then compiler would accept that code. Otherwise it's invalid. In that case we probably can save few keystrokes with the rule you proposed but the whole program would become more fragile: we have to specify these lifetimes expelicitly (because we want to establish relationships between them) – and it helps to see them in struct declaration.

And here the rest of the program, for the completeness (in case if something would happen to Godbolt):

fn foo<'a>(use_first: bool, user1: &'a str, user2: &'a str) -> &'a str {
    let email1: &str = &String::from("foo@bar.baz");
    let email2: &str = &String::from("hello@example.com");

    let (user, email) = select_use_and_email(
        Users { user1, email1, user2, email2 },
        use_first
    );

    println!("e-mail selected: {email}");
    user
    // Variables email1 and email 2 are destroyed here
}

pub fn main() {
    for alice_or_bob in [true, false] {
        let alice: &str = &String::from("Alice");
        let bob: &str = &String::from("Bob");
        let user = foo(alice_or_bob, alice, bob);
        println!("User selected: {user}");
        // Variables alice and bob are destroyed here
    }
}

Lifetimes in Rust are kinda… anti-GC susbsystem.

Where in “normal” language with tracing GC objects exist as long as references to objects exist, that is, additional references extend the lifetime of objects, in Rust it's the opposite: objects exist as scopes, functions and other “big” things make them exist – and when object disappears references should disappear, too (or else program wouldn't work).

My own mental model for “tracing GC languages” are “helium balloons in the sky”: they all “naturally want” to fly away, but as long as they are tethered by at least one rope to the Earth (or to other balloon or balloons) – they stay with us.

And my mental model for Rust is more of a… “railroad with cargo”: train goes from station A to static B, where it unloads its cargo and is split into separate railwagons to move different cargo somewhere. You still can attach ropes, but… your cries and curses “but I had an important info that train, how dare you destroy it” are thoroughly ignored: train arrived at the end station and it would be dismantled… deal with that.

Rust compiler help you to “deal with that” when you use references, but if you use “references evil twins”, pointers… then it's your responsibility to track train schedule and deal with them – yet rule is still the same: cargo moves on trains, trains are dismantled when they arrive at the end station, ropes are useless if they go to the train that no longer exist.


  1. Technically it carries 'static lifetime, but that's misnomer: any type that may exist forever, like i32 or f64 also carries 'static lifetime. ↩︎

2 Likes
struct InvarianceProblem<'a, 'b> {
    foo: &'a mut Foo<'b>,
    bar: &'a i32,
}

If you would have Foo<'a>, your would implicate bar and generally have a tough time with the borrow checker, because 'a would become invariant, while there invariance is limited to just 'b. In the past nomicon had a great chapter on variance, but for some reason it was extremely nerfed that I was forced to look for the old chapter to teach my friend :woozy_face:

1 Like

You don't need to add a lifetime specifier. This a bad suggestion that pushes you deeper into an incorrect design of the struct.

The real error is in trying to use &str instead of String inside a struct that is supposed to own the string.

&str in Rust is not a string by itself, but a loan that has a temporary permission to view string data stored elsewhere.

username: &str means that the struct with this field will not store the username. It is forbidden from storing the username string. It's only given a permission to temporarily refer to a username that exists elsewhere, in some other type, or in some variable. This permission is always very strictly limited, and can't be extended beyond lifetime of the location it is a view into.

If all of this sounds nonsensical and overcomplicated, because it is. It's a wrong design, that uses wrong language feature for an incompatible purpose.

The correct design is:

struct User {
    username: String
}

which allows User to have a username, and therefore it's not borrowing from anywhere, and doesn't need to be a temporary view, it isn't limited by lifetime of anything else, so it doesn't need lifetime annotations to track lifetime of an external location.

You run into this problem, and get very misleading suggestions from the compiler, because string literals are a special case. You start out from an edge case, and then then the compiler suggests generalizing it in a wrong direction.

A string literal like "foo" is not a self-contained String, it's &'static str. It's a loan, but an unusual one, because it's not a temporary loan. String literals are borrowing data from the program's executable, and the executable will stay in memory for the entire duration of program's execution, so string literals have effectively an unlimited lifetime. This isn't true for most other data types in Rust, which will be borrowed from variables that have temporary lifetime, or borrowed for a limited time to allow data to be mutated or freed after the temporary loan ends.

1 Like

If we are going with discussions about “correct designs” and “right directions” then it should be noted that Rust (and most other programming languages) have more things in common with English that we like to admit.

I mean: English is nice, regular language, where you can turn singular into plural with s and present tense verb into past tense verb with ed… but try to do that with simplest, most trivial words. Like “Child is in school”. Plural would be “Childs is in school”, right? Nope, that's wrong. And you definitely wouldn't say “Childs ised in school yesterday”, right?

Rust is the same way: there are rules and they are usually rigorously followed… except with most common, most used parts. There you have special exceptions which help people with experience, but frustrate newcomers to no end.