Hello everyone, can someone please explain how lifetime assignment exactly works here and how all this weirdness leads to the compile error that we have here...?
fn strtok<'a>(haystack: &'a mut &'a str, c: char) -> &'a str {
if let Some(v) = haystack.find(c) {
let first = &haystack[..v];
*haystack = &haystack[(v + c.len_utf8())..];
first
} else {
return *haystack;
}
}
fn main() {
let mut x = "hello world";
// &'a mut &'a str -> what the function expects
// &'x mut &'static str -> what I am providing
let hello = strtok(&mut x, ' ');
assert_eq!(hello, "hello");
assert_eq!(x, "world");
}
Putting the same lifetime name on a &mut plus on anything else is extremely restrictive, and very often a mistake.
When multiple loans have the same lifetime attached to them, they are all required to meet all of the restrictions of all of the loans at the same time. Don't recycle 'a where it's not necessary, because that makes borrowing restrictions exponentially worse.
Lifetimes of exclusive loans (&'a mut) are invariant, meaning they have to match exactly, and cannot be any longer or any shorter.
'static is longer than the invariant 'a, so it won't be compatible with it.
Lifetimes of shared loans (if only shared ones are involved) aren't that strict and the compiler will shorten them if necessary, but if you throw any mut into the mix you ask the compiler to be extra inflexible about lifetimes.
What happens here is type deduction that goes back from the call point to the variable declaration point.
That doesn't happen in most languages thus it's hard to imagine that it would happen… and yet it does.
Because haystack ties the lifetime of mutable borrow to lifetime of x, itself! That means that mutable borrow that you have passed into strtok exist for the exact same time as x!
And now, when you try to call assert_eq!(x, "world"); you have a problem: x clearly exist here, means mutable borrow also have to exist… and it would conflict with an attempt to create an immutable borrow here!
Nit: lifetimes of things under an exclusive loan are invariant (e.g. 'b in &'a mut &'b () or 'a in &'a mut &'a str). I'd take "lifetimes of exclusive loans" as referring to 'a in &'a mut &'b (), which is still covariant.
fn strtok<'a>(haystack: &'a mut &'a str, c: char) {}
fn example<'s>(mut x: &'s str) {
let mut y = x;
// why does `&mut y` still exists after this call?
strtok(&mut y, ' ');
println!("{y}");
}
I modified strtok and made it NOT returning anything. &mut y didn't get assigned to any variable locally and shouldn't it be out of scope after the call?
You mean, shouldn't &mut y be out of scope after the call? It is, but that doesn't matter.[1]
You have to create the &'l mut &'l str to pass strtok to. It's created just before the call to strtok is invoked. It doesn't matter what happens after that -- whether something is returned or not, y is only usable via the &'l mut &'l str after that.[2]
There's no "taking back" the creation of the exclusive borrow, and the borrow is required to be for the rest of y's validity as per the function signature.
The borrow checker determines where things are borrowed not just based on the use of values, but also by respecting lifetime annotations (including function signatures). This fails even though nothing is done with the reference.
fn main() {
let local = ();
let _: &'static () = &local;
}
It is in the original too, you return a reborrow of **haystack. But perhaps that's a distraction here. ↩︎
In the returning version, you can use the returned value; it is still "via" the &'l mut &'l str. ↩︎
Lifetimes don't affect the generated code[1], thus it's not really clear what that question even means.
Yes, it would. That's why compiler adds implicit bound there. You can add explicit where clause, but it's not needed, compiler knows that reference shouldn't live longer than referent.
Except for some corner-cases related to HRBT — and then it affects types of functions picked in some operations, not generated code directly. ↩︎
Of course. That's precisely why things fail: to be correct you need 's: 'm and code, as written, supplies 'm: 's thus the whole thing explodes at compile time.
Yes. By using the same name you adding 'm: 's requirement and because there are already 's: 'm implicit requirement type of your variable is not permanently tied to the type of borrow.
It's easy to see, as I already wrote: just try to declare your variable like this:
let mut x: &'static str = "hello world";
Compiler would immediately yell at you:
error[E0597]: `x` does not live long enough
--> <source>:15:24
|
12 | let mut x: &'static str = "hello world";
| ----- ------------ type annotation requires that `x` is borrowed for `'static`
| |
| binding `x` declared here
...
15 | let hello = strtok(&mut x, ' ');
| ^^^^^^ borrowed value does not live long enough
You have told the compiler that lifetime of reference in x is the same as lifetime of borrow… and then you insist that lifetime is 'static… oops, these are not compatible!
This happens if you just simply call your strtok without doing anything else.
If you don't insist that x contains reference to 'static the compiler can go one step further: it makes reference stored in x short-lived, the same as &mut borrow… but then it no longer can be used when said borrow expires!
Nit: what's actually restricting is putting that lifetime on the type pointed by a &mut reference! The "anything else" then happens to be the &mut itself, which just makes this even worse.
was that &'m mut _ needs to coerce to &'s mut _ which implies 'm: 's, and you can't borrow a local for as long or longer than a nameable lifetime like 's. But what you said is also true, there's a 's: 'm requirement as well, so the 'm = 's is the only possibility (which still fails because you can't borrow a local for that long).
Probably it would have been more clear if I just used 's and didn't mention 'm.
If it would got away after the second line then y would also stop being usable after that second line.
Your program would still be rejected, just for different reason.
Talking about what happens with invalid program, rejected by compiler is always tricky, because… well, it's invalid, it have no meaning — means there could be many different alternate explanations about why it's invalid. Compiler usually only picks one.
we have &'m mut str &'s str (which is &'1 mut str &'1 str in this case) but due to the signature only accepting 'a we have this:
=> we know that 's should live as long 'm lives
=> but 's is 'm (cause signature accepts 'a)
=> therefore 'm (the mutable borrow) should stay active even if the function doesn't return anything, correct?