Function local variable doesn't live long enough


#1

Hello,

I am getting the following error:
error: b does not live long enough

Why can’t I bind p to another reference? Why should b live beyond test()? Isn’t p itself local to test()?

Any help?



fn test(mut p: &mut i32) {

	*p = *p + 1;

	let mut b = 88;
    
    p = &mut b;
    
    *p = *p + 1;
    
    println!("{:?}", b);

}


fn main() {

	let mut a = 5;

	test(&mut a);

	println!("{:?}", a);
}


#2

The problem is that the function type signature you wrote is not the whole actual type signature used by the compiler because of lifetime elision. The expanded signature is

fn test<'a>(mut p: &'a mut i32)

which as you can see is generic over the lifetime. When you call it in main it gets instantiated with the lifetime of the temporary &mut a you construct, temporaries have their lifetime set to be the entire line in which they’re contained. So 'a becomes a lifetime from before test is invoked until after it has returned, which is longer than the lifetime in which b lives.

A workaround in this case is to shadow p inside test with let p = &mut b;, so you have an actual independent variable (with the same name) and rely on the optimizer to reuse the same stack space/register for both variables.


#3

Thank you for your answer.

But isn’t the maximal lifetime of the borrow &mut a equal to the lifetime of the function test() itself? In other words, I was expecting mut p reference to be dropped (and the borrow released) when test() ends. I mean I still don’t understand why the resource b, which is later referenced by p, is still expected to persist after the drop of its borrower, which is p? Who is borrowing b? My understanding is that it is p. You (and the compiler) are implying that b is borrowed by something living outside the scope of b, i.e. the scope of the function itself.

Thanks


#4

Hi,

My understanding is that when you do:

p = &mut b;

p is now a reference to the local variable b.
However, p exists outside of test (it is actually a in your main).
So if you switch a to refer to b within your test, once you return from your test, your reference is invalid.

That’s why Nemo157 suggests to shadow p in your test to not impact a.
You could also *p = b to copy the content.


#5

No. Actually, it is because p is created before b, so b must be destroyed before p, which invalidates the reference in p.

Thank you all.


#6

kimsnj,

p is local to test() but it is initially holding a reference to a which is living in main. But later I choose to let p reference a variable called b which lives in test(). The problem is that “values in a scope are dropped in the opposite order they are created”; therefore, b must be dropped before p which is still referencing it and it is invalidating the reference.

I don’t know but if the compiler was smart enough, it should be able to figure out that p (although becoming invalid when b is dropped) cannot possibly be used anymore, being local to test(). What is the technical reason behind not allowing a reference to a non-existing resource if it is knowable that that reference is being dropped itself anyway and cannot possibly be used?


#7

Didn’t know this. Thanks for the clarifications :slight_smile:


#8

I didn’t actually think about the drop order, but interestingly rust does tell you about that when everything’s local to a function: https://is.gd/HejPOe

In the initial code this would be an issue, but it’s being hidden behind the issue with the generic lifetime I posted. The lifetime 'a in the expanded signature of test is being set to the lifetime of the &mut a temporary variable inside main, which is for the entire statement test(&mut a);, from just before test is invoked until just after test returns. b inside test has a lifetime from the let b until just before test returns, the line p = &mut b is sort of like p = &'b mut b where 'b: 'a, the lifetime of the reference assigned to p must live at least as long as p says it will, and it’s impossible to satisfy that constraint as a reference can live only as long as the data it’s referencing and b's lifetime ends before 'a ends.

This is very subtle behaviour that is definitely not needed to be understood when you’re learning rust. In practice it should be fine to just use the drop order rule to understand this sort of issue. But understanding how this works might help you once you start to really master the borrow checker.


#9

Nemo157, I don’t see why the borrowing of b should be affected by the lifetime 'a which is that of the test() function itself – and of the temp var in main. Since b is referenced by the local p, the lifetime of b is only required to be at least equal to the lifetime of p which is referencing it – and should not therefore be affected by the lifetime 'a of test() and main’s temp variable as you are suggesting.

Now since b is being dropped inevitably before p, the compiler is complaining that it is not living long enough. This is my understanding, unless you can tell me why would the lifetime of b be bound to the lifetime 'a instead of the lifetime of p which actually borrowed it.

Correct me if I’m wrong.

Thanks


#10

The issue isn’t that b has a shorter lifetime than p (that would be an issue, but it errors out before checking that), but that b has a shorter lifetime than the lifetime required by the type of p. p itself only lives from just after test is invoked until just before test returns, but its type is &'a mut p in the expanded function signature and 'a is a lifetime that extends outside of test.

https://is.gd/y14mF5 represents this a little better, even though b is declared before p, and therefore outlives p, you still can’t assign p = &mut b as the borrow checker can’t prove that b outlives 'a (you can remove the lifetime from the function and it is identical, but the error message will reference an anonymous lifetime instead of 'a).

I was a little off in implying that the error is because 'a is set to be the lifetime of the temporary variable in main, rather the input lifetime parameters to a function must outlive the function invocation itself, so the borrow checker can deny this code just by looking at the test function on its own.

As I said though, this is really subtle behaviour and I’m completely failing to come up with a really good example for it; I think because there is actually no way for this way of thinking about it to produce different behaviour to thinking about it in terms of when the variables are dropped, other than this super-edge case of argument order.


#11

Alright. Thanks for the information.


#12

The code in question is analogous to returning a reference to a local from a function - it’s invalid. ‘p’ is initially a mutable reference to a stack slot from ‘main’. ‘test’ receives that reference and then makes it point to a stack location of a local that will be destroyed once its lifetime ends, which is before the lifetime of ‘p’. When that happens, ‘p’ is left pointing at an invalid address.


#13

But my point and my understanding actually is that p is local to test() and that it cannot be bound by any lifetime exceeding test(). Although p initially is assigned the address of a memory on main’s stack, it is later changed to point to the local b. What I don’t understand is why would p as such (and not the resource it was initially pointing at) have to have a lifetime exceeding that of test() ?


#14

Lifetimes are not dynamic. You can’t change the lifetime of a borrowed pointer, no way, no how. When you re-assign, the compiler has to either narrow the lifetime so it matches the existing lifetime on p (which it can’t here), or fail (which it does).


#15

Why couldn’t the compiler insert the appropriate code to narrow the lifetime of p right after where the assignment occurs? Shouldn’t a pointer’s lifetime and validity after all be bound to what it is pointing to – since that which a pointer may be pointing to can actually change, as allowed by the language? In other words, are there any semantic or technical reasons behind Rust lifetimes not being dynamic?


#16

Having the lifetime be part of the type (and thus, statically verifiable) is one of the major defining features of Rust. You might as well ask why Rust doesn’t change the type of a variable from an i32 to a String when you try to assign the wrong type: because those just are not the semantics the core devs want the language to have.

Heck, those are semantics I (and I would hazard to say most of the people using Rust) don’t want the language to have.

Though that does make me wonder: are you aware you can re-bind variables?

let p = &mut b;

#17

But the re-assignment of p in my example is statically verifiable. In other words, the compiler could accurately determine how to narrow down the lifetime of p after it has been reassigned. Unless I’m missing something. And I did not borrow from another type in my example, so I don’t see the relevance of your comment to our issue here – i.e. b is also i32.

Thank you


#18

‘p’ is an alias to something the caller defined. If you change the address of what ‘p’ points to, you’re changing what the caller sees. Since you’re making it point to a local, the caller will be left with a reference to free’d/garbage memory.


#19

p is local to the called function and is dropped when the function itself ends. You are assuming we are passing the address of a pointer, when the function is called, which is not the case; only passing an address. We are only copying an address into p, which lives on the function’s stack, and then changing this address to point to a local memory. I don’t see how the caller could still have access to (the address stored in) p which lives on the function’s stack.


#20

You’re right. I guess the issue is, as mentioned upthread, is the difference in lifetimes between p and b mixed with lexical borrow scope?