Necessity of lifetime annotations


#1

I’m reasoning a lot why exactly the compiler needs lifetime annotations in specific situations. After reading all the docs about lifetimes and testing use cases, I wonder why the compiler wants me to correctly insert lifetime annotations although he himself is able to correctly figure out the correct lifetimes.

Am I right that, lifetime annotations are not necessary for the compiler/borrow checker and the only purpose for the annotations is to guide human implementors and users of functions in reasoning about the resulting borrow/lifetime tracks of references in a program?

If yes, shouldn’t the documentation and error messages of lifetimes mention this instead of claiming its inability to infer lifetimes?
(e.g. https://play.rust-lang.org/?gist=5be79d6eaf771b6c7d68&version=stable)


#2
fn print_one<'a>(x: &'a i32) {
fn add_one<'a>(x: &'a mut i32) {
fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {

Lifetime elision makes these unnecessary.

fn pass_x<'a>(x: &'a i32, y: &i32) -> &'a i32 {

I suppose the compiler could be able to infer the return borrow lifetime from the function’s body but that would mean the actual signature would depend on the implementation (would you want that?) and lead to non-local error messages (the code expecting the return value to live as long as x could randomly break because of bugs in the pass_x body).


#3

It’s not actually possible for the compiler to figure out the correct lifetimes in all cases. That’s why they’re needed. A quick example:

struct Foo1<'a> {
    bar: &'a i32,
    baz: &'a i32,
} 
struct Foo2<'a, 'b> {
    bar: &'a i32,
    baz: &'b i32,
} 
struct Foo3<'a, 'b: 'a> { 
    bar: &'a i32, 
    baz: &'b i32, 
} 

These are three different structs containing two references, but the lifetime interactions are all different. The compiler can’t know which one of the three you want.


#4

Hi Steve,

I think your getting my point, is the second struct intentionally named like the first one and is, because of the lifetime “signature”, a different type?


#5

I named them differently so that they could all be run at the same time. They are three different structs with three different semantics about lifetimes. Imagine you needed a Foo in your program, 1, 2, and 3 are all valid choices, but mean different things.

The first says “both references have the same lifetime”, the second says “each reference has a different lifetime” and the third says "each reference has a different lifetime, but the lifetime of baz is at least as long as the lifetime of bar.


#6

Why aren’t they all named Foo1 then? Or was the 2nd one meant to be Foo2 ?


#7

Oh yeah, I meant 1, 2, 3, and just made an error :frowning:


#8

But, when they have different names, there’s no problem for the compiler at all. Could you please write a small compilable (but with errors) example which illustrates where the compiler, borrow checker cannot infer the intended lifetime, function or struct without the 'a thing?
My actual mind is, when the compiler wouldn’t be able to exactly track refs&borrows it wouldn’t be safe.
So my annotations are possibly needed for ambiguities, but which?
Or is this a linkage thing with different compiled units?

Thanks for your time!


#9

Think of the lifetimes as part of the reference’s type. Asking the compiler to infer the lifetimes of all references is like asking to be able to write the struct like this:

struct Foo { 
    bar,
    baz,
}

and to have the compiler infer the types of the struct fields. It might be possible in some cases, but it goes against the philosophy of Rust. It might result in the inferred types being either to restrictive (think generics) or too broad (because of type errors that would be caught). This is especially bad when you want to provide the struct as part of the public API of your crate.

The fact that you can omit lifetimes in certain function signatures is due to elision, which follows simple mechanical rules, with no inference involved. (It’s like saying "you can omit an argument type if it’s int.)


#10

Right, this is why I wrote:

When you’re writing a program, you need to pick which semantics you want. Those three structs are three examples of different ways that the lifetimes could be annotated, and why the compiler can’t just figure out what the right one is: all three are valid.


#11

Hi Birkenfeld,

with generic functions AFAIK, I can be abstract about real data types, with the benefit of having the compiler generate the needed real typed versions of this function for me.
With traits, I can be abstract in function arguments, by constraining to a set of necessary methods.
With a static typed language, I get the benefit of having clear interfaces to functions, got it.

But in which way, I can be abstract about the lifetime of my input or output references. Aren’t these already determined by the control flow of my program? Are these 'a-notations to constrain the possible input references? I’m sorry I can’t imagine a real world example.


#12

Hi Steve,

I think we’re getting closer to my misunderstanding :wink:

Ok with these two different lifetime notated structs, I can construct Foo1 structs which two initializers must have the same lifetime right?
What is my benefit of that?


#13

An example would be you could have a function that returns &'a i32 that conditionally yields bar or baz. Otherwise it’s usually just more convenient to “collapse” the lifetimes into one if you don’t need them to be distinct (but this is a side-effect of needing to specify them at all).

Also note that they don’t have to have the same lifetime. Rust tries to make lifetimes as small as possible, but will happily make them bigger to satisfy constraints. Once they’re put in the struct the compiler will try to stretch their lifetimes to satisfy this constraint. Since lifetimes are just a region of code, the resulting lifetime will just be some minimum cover over their two regions. If one of the references can’t actually live this long, you’ll get an error.


#14

Hi Gankro,

I think you mean something like this:
https://play.rust-lang.org/?gist=56b32737c573789d4689&version=stable
But in this case I’m not allowed to use different lifetimes…


#15

@androck yes that’s exactly the case where you should only use one lifetime. This allows the program to work (I thought you were asking why you would want to only use one lifetime?)


#16

Hi Gankro,

I want to understand the technical necessity of the lifetime annotations. This helps people to obey work intensive rules :smile:

The Question in other words is:
Does Rust’s language paradigm/design, by sticking to non-GC, memory-safety and enforced ownership/borrowing rules, consequently lead to the necessity that users must annotate the lifetime in some situations. And if it does, I’m seeking for an example that illustrates that.

All examples I have seen so far, seems to me that the compiler always knows how to obey the ownership/borrowing rules. Steve and some others told me that there are situations where the compiler doesn’t know.

My former thesis was that, the annotations are just like function declarations a sort of user-implementor contract, saying e.g.
int test(double x); // means: you the user promise you give me a double, and I the implementor return you an int. And the compiler only enforces the implementor/users to hold their promise.

But especially in the case of structs this assumption makes no sense. I can not get the need for annotations there.

struct Foo1<'a> {
    bar: &'a i32,
    baz: &'a i32,
} 
struct Foo2<'a, 'b> {
    bar: &'a i32,
    baz: &'b i32,
}

By now, it makes no sense for me here to annotate that.
Every borrowed element has to outlive the struct, in any case right?
Are we here enforcing to only take borrows which have a special longer lifetime as just at least the structs lifetime?
Or are we extending the lease time over the structs lifetime?
In which case makes it sense to say that element baz has to life longer as element bar?


#17

Oh ok. Here’s a simple case:

fn foo(x: &u8, y: &u8) -> &u8;

who is borrowed, x or y?

Now

struct Foo(x: &u8, y: &u8);

impl Foo {
    fn foo(&self) -> &u8;
}

what lifetime is the return value valid for? There are 3 choices, and they’re all valid with different consequences:

struct Foo<'a, 'b>(x: &'a u8, y: &'b u8);

impl<'a, 'b> Foo {
    /// Can return only x, won't borrow self (copies the ref)
    fn foo1<'c>(&'c self) -> &'a u8;
    /// Can return only y, won't borrow self (copies the ref)
    fn foo2<'c>(&'c self) -> &'b u8;
    /// Can return x or y, but this borrows self
    fn foo3<'c>(&'c self) -> &'c u8;
}

A concrete example where this matters is Iterators. Consider a simple IterMut implementation for slices:

use std::mem::replace;

pub struct IterMut<'a, T: 'a> { data: &'a mut[T] };

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;
    fn next<'b>(&'b mut self) -> Option<Self::Item> {
        let d = replace(&mut self.data, &mut []);
        if d.is_empty() { return None; }

        let (l, r) = d.split_at_mut(1);
        self.data = r;
        l.get_mut(0)
    }
}

Note that we return &'a muts, but call on &'b mut self. This serves a dual purpose:

  • our returned values can outlive the iterator (it can be dropped, and we live on)
  • our returned values do not borrow the iterator (so you can call next without destroying the yielded elements)

these are non-trivial API decisions that the compiler can’t make for you

You could imagine having an iterator interface like next<'a>(&'a mut self) -> Option<&'a mut Self::Item> which makes the elements borrow the iterator. See http://cglab.ca/~abeinges/talks/iter/#15
for details.


#18

Perfect, now I got it thank you Gankro!

Very good explanation.


#19

@steveklabnik

As far as I now understand, we’re not constraining lifetimes. We annotate leasetimes of our borrows.
Please consider an example like @Gankro showed (or from his nice slides) to your very good Rust book’s.

Maybe it is useful to talk of lifetimes only in context of ownership, and use the term leasetime where borrows are discussed? From my Point of view, it would have helped me to understand the concept and it’s necessity.


#20

What is the difference between a “lifetime” and a “leasetime” here?