Need help understanding a lifetime concept

fn main() {
                        //lifetime a
    let x=10;
    let y=30;
    let res:&i32;

    {//lifetime b
        let inner_x=5;
        res=smaller(&inner_x,&x);
        println!("{}",*res);
    }
   


}


fn smaller<'a>(i:&'a i32,j:&'a i32)->&'a i32
{

if *i<*j{
    i
}
else{
    j
}

}

As far as I understand ,the function signature of smaller takes references to two i32
whose lifetimes are 'a.
But in the above code we are passing x and inner_x which have different lifetimes.
I understand the code but I'm not able to figure out the use of 'a in function signature.Thanks in advance!

References can be implicitly converted to shorter lifetimes, so even if the two references had different lifetimes, upon calling smaller they can be both shortened to the smaller (or the intersection) of the two lifetimes and thus become the same.

In this case however, the lifetimes might as well be the same to begin with. A lifetime of a reference does not reflect how long the original variable you borrowed from lives, but the reference can (and often will) be way shorter. The compiler automatically infers appropriate lifetimes, so arguably, for those two references being created just for the call to smaller anyways, their lifetimes may have been the same to begin with.

3 Likes

Thanks for the reply! I'm sorry but as a newbie I didn't understand the explanation.Can you suggest me some reading material and languages where lifetime concept is utilized?

In case you haven’t read the book yet, that’s generally a good recommendation for learning the basics of Rust, and there’s also a chapter about lifetimes in it: Validating References with Lifetimes - The Rust Programming Language.

A somewhat “advanced” source that talks about how references can be converted to shorter references, and the more general mechanism of variance is this chapter in the Rustonomicon: Subtyping and Variance - The Rustonomicon

If you need help understanding my explanation, you can also ask questions or point out concrete points that are unclear to you and perhaps in what way / for what reason they’re unclear; that way it would know what part of my explanation I could/should elaborate on, or for what specific topic further material might be useful.

2 Likes

Thanks for the suggestion.I am currently reading the book and I was just experimenting with some test examples.I would surely checkout the other link.

In this case however, the lifetimes might as well be the same to begin with.

How can the lifetimes be the same initially? Could you please explain?

I’m talking about the lifetime of the references created via the expressions &inner_x and &x. The lifetime of a reference determines for how long, i.e. up to which point in the code, the reference is considered to be “alive” and thus usable. The lifetime of the reference is different from the scope of the variable being borrowed, i.e. the variable can exist for much longer than a particular variable to that reference.

When you create a reference, the compiler automatically chooses an appropriate lifetime for how long that reference will be valid, and it could thus choose that both references here are valid up until the same point (e.g. up to right after the point where res is last used, which is the println! call).

The lifetimes are chosen in a way that avoids violating Rust’s rules for borrowing, i.e. that borrowed things must exist for the duration of the borrow, or that mutable references must have unique access, and it also ensures that lifetime constraints set by function signatures of functions that you use are not violated. If choosing lifetimes in such a manner becomes impossible, you would get a borrow-checking error at compile time.

Feel free to ask more follow-up questions. Maybe you have some ideas why the lifetimes could not be the same, or why some rules for lifetimes / borrowing should be violated by this code. If that’s the case, feel free to share your interpretation, so we can figure out whether your reasoning might be based on any particular misconceptions. Rust’s borrowing system is a bit complex and also something very new to anyone who hasn’t seen it before, so having a somewhat inaccurate understanding initially is completely normal.

1 Like

Thanks for the elaborate reply!.I think I'm understanding it a bit better now.So basically rust does
compile time checks to ensure that all references are valid and there are no "dangling pointers".
The lifetime concept in rust is introduced so that memory safety is guaranteed.
So am I getting it right?

Yes, that sounds generally right.


An IMO very good introduction to some of the core ideas for lifetimes in Rust is also this video on YouTube. Though its premise is that it assumes some familiarity with C++, because its examples work by means of comparing to C++. The main part of the video goes through various examples to motivate, demonstrate, and explain the basic ideas of Rust’s borrowing system, in particular lifetimes (ensuring references are valid) and aliasing rules (how and why mutable references require unique access). And afterwards, the video also covers move semantics and thread safety a little bit. I see in previous questions of yours that you might be familiar with C at least, perhaps even C++, so feel free to look into it and see if you like it.

1 Like

That sounds great!I am familiar with C++ and thanks for the suggestion I'd check it out.

Hey steffahn I just have a doubt .In the following function signature

fn foo(a:&'i i32,b:&'i i32)->&'i i32

Does 'i denotes the lifetime of the reference or does it mean:-" return a reference to i32 value whose lifetime is 'i .I think it is the latter case.Am I correct?

Depending on how you interpret the meaning of “lifetime”, neither might be correct. Besides the lifetime as denoted by 'i, there’s the time the original i32 lives/exists, and there’s also the time the returned reference exists. The lifetime denoted by 'i can be

  • shorter than the time the original i32 exists
  • longer than the time the reference actually happens to exist.

The point of the lifetime argument 'i of a reference however is that it always must

  • be longer than (or equal to) the time the reference actually exists
  • be shorter than (or equal to) the time the target i32 variable exists
    • and in fact also throughout the time indicated by 'i, the target i32 must not be mutably accessed.

So to relate as much as possible to your question’s wording, arguable, it is the case that

  • 'i denotes an upper bound to the lifetime of the reference (the caller cannot keep the returned reference alive for longer than 'i), and
  • the function returns a reference to an i32 value whose lifetime is at least 'i (so that if the caller does keep the reference alive for up to 'i, it’s safe to use and pointing to a valid target all the time).

The reason why it’s not indicating exactly either of the actual run-time “life-times” of the target or the reference is that borrow-checking is a static (i.e. compile-time) thing, and thus lifetimes denoted by lifetime parameters must be a particular “region of code” determined at compile time; while the time of actual “life” of a value or reference is something that ultimately only gets decided at run-time, and might depend on input, if there’s e.g. if or match expressions acting on the value or reference conditionally.


Some more concrete examples of how 'i can differ from either as described above:

For references existing shorter than their lifetime: you often have &'static str value in Rust from string literals, and while the underlying target string data lives for the 'static duration, i.e. for the whole duration of the program, the &'static str references themself very often live a lot shorter.

On values existing longer than a reference’s lifetimes pointing to them: Reference lifetimes can be shortened through coercion. A &'long T reference can be converted into a &'short T reference. But of course, even after this conversion, the underlying value will still exist for at least the 'long lifetime; just the new &'short T reference does not reveal this fact to its users anymore.

And, as I mentioned before, functions like foo that require two references with the same lifetime parameter can be called with references that originally had different lifetime parameters, by coercing one (or both) arguments to a common sub-lifetime, i.e. a lifetime parameter shorter (or equal) than both original ones. The actual targets of the longer-lived original references, which might become targets of the return value of foo, too, would of course still be actually alife for (at least) as long as their original lifetime arguments indicated. Or foo might just as well return a reference into static memory, something that was originally a &'static i32; and with Rust’s language features (implicit constant promotion) doing so is actually quite straightforward, and as simple as writing something like “&42.

2 Likes

This is a bit tangential to your question:

Hopefully this doesn't confuse things further, but I think it's important to mention that the lifetimes for foo are generic. It should be written as:

fn foo<'i>(a: &'i i32, b: &'i i32) -> &'i i32
//    ^^^^   

What this means is that you have to think about foo generically. It's an API constraint, I suppose. The code you write needs to work no matter which lifetime the caller's parameters indicates for 'i (as long as a and b both have the same lifetime).

Writing generic code is both powerful and limiting. Consider this example without lifetimes where I ignore my input and return a value:

fn foo<T>(_a: T) -> i32 {
    // I can ignore _a and still meet foo's type signature 
    // because I know how to construct an `i32`. Like so:
    42i32
}

This doesn't work with generic return types though:

fn foo<T>(a: T) -> T {
    // I know T is a type, but I don't know which one it is
    // so I can't create my own T. The only `T` I have access
    // to is `a`
    a
}

This is the sort of constraint you work under with lifetimes too. If you're being generic over a lifetime (which will be the case whenever you're not working with 'static), then you will generally be constructing lifetimes in your outputs from your inputs.

1 Like

Thanks for the elaborate explanation steffahn! I will have to read this a few more times to get the hang of the concept and would use this as a reference.

I am getting it so basically the function signature is a contract which the caller has to satisify for different input lifetimes.Thanks for the reply!