fn longest<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() -> Result<(), StdSafeDynError> {
let myS1 = String::from("xxxx");
let myS1Ref = &myS1;
{
let myS2 = String::from("yyy");
let res = longest(myS1Ref, &myS2);
DEBUG!("{:?} is the longest", res);
}
Ok(())
}
Does 'b: 'a mean the relationship between lifetimes is 'b >= 'a?
If that's the case, why doesn't the compiler report an error, even though the actual argument 'myS1Ref clearly outlives 'myS2?
Someone told me:
The compiler does not rigidly assign 'a to the "first parameter" and 'b to the "second parameter."
When handling a generic function with the constraint 'b: 'a, Rust unifies all constraints based on the actual lifetimes of the arguments. As long as the final result satisfies 'b: 'a (i.e., 'b is at least as long as 'a), there will be no errors.
Thus, the confusion of "how could the first argument (the longer reference) be inferred as 'a, and the second argument (the shorter reference) as 'b" arises because the compiler might swap the roles of 'a and 'b to ensure 'b: 'a is not violated.
When you call longest the lifetimes of the parameters can be shortened. I. e. the function actually receives only a copy of the reference with shorter lifetime than the actual lifetime of th reference you gave it.
Also in this particular code, there's no reason why myS1Ref must be living longer than &myS2 to begin with. They are both last used (directly) at the call to longest. It's also helpful to understand that lifetimes and lifetime marks in Rust only reason about the end of the lifetime of a reference. Whether one reference was created earlier than another doesn't really matter at all.
Lifetime parameters don't necessarily have to be equal to the liveness of the reference it is passed. The compiler can use subtyping to shorten the lifetime of a reference:
I tried a few times to write a succinct reply, but failed. So apologies in advance for the wall of text. I hope it is at least useful.
It looks like you're experimenting based on the chapter of the Book that introduces lifetimes. That chapter strongly conflates the liveness scope of variables and Rust lifetimes, which may lead to some confusion and misunderstanding.
Rust lifetimes -- those '_ things -- are primarily about the duration of borrows. They are not directly about the liveness scope of variables, and the liveness scope is not associated with a Rust lifetime. The actual way the borrow checker works is to figure out where and how (shared or exclusive) places such as variables are borrowed, and then to check every use of ever place to see if it conflicts with any borrows.
The longest examples in the chapter which fail do so because going out of scope is a type of use which conflicts with being borrowed. The chapter makes it sound like this is the primary consideration, and that preventing dangling references is the only point, but that's not true. Moves, overwrites, and taking a &mut _ are also exclusive uses that would conflict, just as going out of scope does.
The chapter makes going out of scope sound a lot more involved with lifetimes directly than it really is. Going out of scope is a use which can conflict with being borrowed. It's checked after the lifetimes have been determined by other means. There are other ways to think about it, but that's how the borrow checker is actually implemented.
The book is trying to present a simplified mental model to get the gist of borrowing and lifetime annotations across (but missed the mark IMNSHO).
The book makes it sound like it looks at the liveness scope of something -- where myS1 and myS2 go out of scope, perhaps, or where myS1Ref goes out of scope -- when calling longest. It does not. Instead it imposes a relationship between the return type and the lifetimes on the inputs. Namely, the signature means that the referents of myS1Ref and &myS2 must remain borrowed so long as res is in use.
Your extra lifetime and 'b: 'a annotation doesn't actually make a practical difference, which the other posts are trying to explain. I'll return to that in just a bit.
The lifetime on myS1Ref is the duration of the borrow of myS1. It is true that if the program compiles, this lifetime cannot include the part of the code where myS1 goes out of scope, as going out of scope conflicts with being borrowed.
But myS1Ref can go out of scope before the borrow ends -- that's why you can put a &'static str in a local variable, say -- or even after the borrow ends, because references going out of scope do not "use" their referents. They don't cause the lifetime in their type -- the borrow of their referent -- to be active when going out of scope.
So the liveness scopes of references matters even less than that of types with non-trivial destructors. A reference going out of scope can cause a borrow check error if the reference itself is borrowed -- if you have a &&T or something -- but that's pretty much it.
I'm going to explain what the borrow checker does for the case with one lifetime, 'a. Then I'll return to your OP with the extra lifetime.
Let's say that the type of myS1Ref has a lifetime 's1, the &myS2 expression has a lifetime 's2, and the the type of res has a lifetime 'r. What happens is that making the call to longest::<'a> and assigning it to res imposes some constraints:
's1: 'a // coercion to argument
's2: 'a // coercion to argument
'a: 'r // assignment from `longest(..)` to `res`
That means, whenever 'a is valid, 's1 and 's2 have to be valid too, and wherever 'r is valid, 'a must be too. Or more practically phrased: so long as res is still in use, myS1 and myS2 must remain borrowed. (The only time 'a shows up is during the assignment to res.)
The borrow checker doesn't choose 's1 and 's2 ahead of time and then pick one when calling longest. Instead, it figures out all the lifetimes based on what gets used where, and by applying the constraints and making sure they can be satisfied.
res is not used after the DEBUG, and references going out of scope is a no-op in terms of the lifetimes in their type. So 'r only needs to be valid through the DEBUG. The two Strings must remain borrowed until that point as well, due to the lifetime constraints. But there's nothing making them be longer -- references going out of scope do not force the lifetime to be active -- so their borrows can end after the DEBUG as well.
Then all the uses are checked, such as myS2 going out of scope. If there are no conflicts, the compilation succeeds.
The book makes it sound like the input lifetimes are known and determine the output lifetime, but as far as the borrow checker works, the opposite is true for this example: the (use of the) output determines what the input lifetimes are.
With that context, what happens when you add the extra lifetime? Now the constraints due to the call to longest::<'a, 'b> are:
'b: 'a // C1 -- your added constraint
's1: 'a // C2 -- as before
's2: 'b // C3 -- as before (but a different lifetime now)
'a: 'r // C4 -- as before
Just like before, where res is used determines where 'r needs to be alive. Due to C4, that also means that 'a needs to be alive. Due to C1 and C2, that means 'b and 's1 need to be alive. And due to C3, 's2 also needs to be alive.
You've just added some indirection between 'r keeping 's2 alive, via 'b. But from a practical perspective, everything works the same. Technically you can pass different lifetimes into longest now, but from a practical perspective, there's no need to and you generally don't want to.
Final note: in neither case was an outlives relationship between 's1 and 's2 created. They have to overlap -- 'r is within the intersection of the two -- but one is not forced to outlive the other. And even if they had been, that would be a constraint on the borrows of myS1 and myS2, and not on the liveness scopes of myS1 and myS2.
Ultimately this is all a constraint satisfaction problem, so it is possible to have a mental model that works the other way around, closer to the book's presentation.[1] That is, with a non-compiling program, it is sort of philosophical question on how you want to describe the problem. For example here:
let string = String::new(); // L1
let borrow = &*string; // L2
let _move = string; // L3
println!("{borrow}"); // L4
Is string still borrowed on L3 due to the use on L4, and the exclusive use on L3 is a conflicting use that causes the error? Or does string cease being borrowed just before L3 due to the exclusive use, and the attempt to use an expired borrow on L4 is the conflict? Both can be valid mental models to understand the error.
But the way the actual implementation works today is, the borrows are calculated before conflicting uses are considered.
But not exactly like it, as one can craft examples that don't jive with interpreting liveness scopes as lifetimes. ↩︎