Why we need lifetime annotation in a struct?

In my understanding , the basic idea of lifetime is : an object must outlives all of its reference(s). In the following example , it is obvious that a must outlives b since b hold a reference to it. The compiler should find out this fact without the lifetime annotation, but it does not , why ? Is there any counter example ? Many thanks!

struct T;
struct S(&T);
fn main() {
  let a = T;
  let b = S(&a);
}

I'm a little confused. Where in the example do you believe you've written a lifetime annotation?

above is what I expected , this is what the compiler required

struct T;
struct S<'a>(&'a T);
fn main() {
  let a = T;
  let b = S(&a);
}

Ah I see.

To explain the reason for that, consider this example:

struct T;
struct Foo(&T, &T);

What is intended here? Should the 2 fields of Foo have the same lifetime, or should each have a different one? To resolve this conflict, Rust requires you to specify which field uses which lifetime.
The same thing holds regardless of how many fields are in the (tuple) struct.

2 Likes

S isn't a struct type. It is a higher kind. With different lifetimes you have different structures. Type inference lets you construct without specifying lifetime. Variance and NLL add a more flex.

the intention is obvious that object(s) referenced by Foo.0 and Foo.1 must outlive(s) object of Foo.

Consider using the types you specified and a hypothetical struct U:

struct U<'a>{
foo: &'a T,
bar: S
}

What is the lifetime of S?

1 Like

Rust requires lifetime annotations anywhere that you have a reference inside of a struct. The lifetimes are used to give an indication of where the data that is referenced is borrowed from. It's like a breadcrumb almost where if you followed the corresponding lifetimes you would eventually find where it is owned, and Rust uses this information to figure out if it can prove that the owned data still exists at every point in your program that you use that reference.

In your example the relationship of the lifetime is simple and easy to understand, but the issue is that in many situations it isn't simple and Rust requires you to be explicit about the lifetime for that reason. It's very similar to the way that Rust requires you to annotate the types of your function arguments. Rust could, for instance, figure out the type of argument a in this example:

fn my_func(a) -> usize {
    a
}

The only possible type of a in this case is usize, but in order to be explicit in your API and clearly represent the boundaries that must be enforced to make the program make sense, Rust requires you to annotate your function argument types.

It's similar with your struct. Your struct has a requirement that that lifetime must be valid for the struct to be valid, and anybody who uses that struct needs to understand that so it is important to annotate it, even in the trivial situations, because things are not always trivial.

7 Likes

Because when you put a temporary borrow inside a struct, the struct as a whole becomes temporary. If it didn't, it could end up referencing something after it's gone (that's a dangling pointer, and it's a security vulnerability). So there has to be a guarantee that the struct will be destroyed before everything that it references is destroyed.

Rust doesn't have a garbage collector, so it can't make struct content live longer if necessary. It can only force struct to live shorter instead.

While the compiler technically doesn't have to require an explicit syntax lifetime in all cases, it absolutely has to track struct's lifetime in all cases. The "infectious" nature of Rust's temporary borrows is necessary for safety. The syntax becomes necessary in some more complicated cases and in generic code where there isn't specific code to analyze.

But the syntax exists and it's deliberately tedious and annoying to remind you that temporary borrows in structs are not easy to use, and shouldn't be used lightly. They come with lots of hard limitations, and you're going to be reminded about that every time you write <'a>.

You don't write these <'a> in other languages, because references in other languages are equivalent to what Rust calls Arc<Mutex<T>> (if they're safe) or *mut T (if they're unsafe). In Rust you also don't need to write lifetime annotations for these other kinds of references. Only scope-limited temporary references need to annotate the scope they're bound to.

6 Likes

I think the question boils down to "Why aren't lifetimes elided for structs?", like the way they are for functions. It's more of a language design question than a question about what lifetimes are and why they're needed.

@kornel does a good job about explaining this language design choice about making structs with lifetimes explicit as a reminder of the limitations that lifetimes involve.
But there's still the question of why functions get to have lifetime elision when structs don't. IIRC, functions didn't always have lifetime elision. But lifetime elision was added because you write functions that use references far more often than you write data structures that use references, and there were certain very repeated/tedious patterns when using lifetimes in functions. These patterns followed clear rules and these rules were implemented in the compiler itself and called lifetime elision.

I might be slightly off about the history of lifetime elisions. I wasn't there, so don't quote me.

4 Likes

Yes, I read somewhere that at least some of the elisions were added with the Rust 2018 edition. I agree that it makes sense to elide those extremely reppettitive patterns that are always the same and simple and intuitive to prove. Lifetimes in structs are far less common, and it is a good point that they make a point of the limitations.

Hypothetically, the language could have been designed to put lifetime parameters in such a struct implicitly. However, it does not. Apparently it was judged to be more trouble than it was worth.

I think a big part of this is that the syntax part of putting lifetimes in structs is the easy part of using them.

As the saying goes,

5 Likes

Yeah, putting lifetimes on a struct has important side-effects, and is something you should be aware of when doing.