I now confused by the invariant of a life time.
At begining, I thought that means 'a will not allow to change to 'b: 'a.
After several trying, I failed to construct a meaningful case.
So I go back to the origin: Future Context, the commence describes fn(&'a ()) -> &'a () is here to make 'a invariant. My question is: where can I observe it's effect.
It is just a struct, not trait. And I can make the following compiled, indicates 'a invariant does not forbid c2.waker = &s1; event they have different life times.
Document says construct is invariant if any field of it is invariant.
Then maybe c1<'a> = c2<'b> will fail, turns out, does not fail either.
#[test]
fn d() {
pub struct Context<'a> {
waker: &'a String,
// Ensure we future-proof against variance changes by forcing
// the lifetime to be invariant (argument-position lifetimes
// are contravariant while return-position lifetimes are
// covariant).
_marker: PhantomData<fn(&'a ()) -> &'a ()>,
}
let mut s1 = "1".to_owned();
let mut c1 = Context {
waker: &s1,
_marker: PhantomData
};
{
let mut s2 = "2".to_owned();
let mut c2 = Context {
waker: &s2,
_marker: PhantomData
};
c2.waker = &s1;
c1 = c2;
}
}
Why not? Let's say everything before the inner block is given a lifetime 'a that ends at the end of the inner block. When the items inside the inner block are created, they can be given the lifetime 'a as well, with no violations and while respecting (in)variance.
NLL can be pretty clever in finding a set of lifetimes that work -- I've been challenged myself trying to set up an example where some sort of lifetime rule is violated. That said, it's explained in the NLL RFC. It's a lot to read to understand the context, but it has a section showing that invariance is just an additional constraint in the system (no extra logic beyond that is required).
(Note: 'a: 'b and 'b: 'a together implies 'a == 'b.)
For an alternative, even more flexible take on NLL, you can read a blog series on the next-generation borrow checker, Polonius:
This is again a lot of reading. The first one sets the general idea and context, and the second one covers lifetime relationships. (The third one is about higher-ranked types.) Variance isn't specifically addressed in these posts, but as in the NLL RFC, it's just a detail concerning the subset relationships between lifetimes.
Lifetime is not "from this until that". Lifetime is only "until that", since by the very existence of the lifetime-annotated reference you guaratee that the object exists at the current point - the important question is "when it ceases to exist", not the exact span.
Sure it is. Or, it can be, and since that lets the whole thing compile (because of c2.f = c1.f) the compiler can just make them the same.
The actual lifetimes of s1 and s2 are controlled by you, because of the scopes you declare them in and the order in which you manually call drop. But the lifetime parameters of c1 and c2 are chosen by the compiler, and are only bounded above by s1 and s2.
When you write &'a T in a signature, 'a does not mean "the duration of time this Tlives for": it means "the duration of time I am borrowing this T for", which may be shorter.
Of course I can, since this code would not change its semantics if these two drops are swapped. Otherwise, no two lifetime would ever be identical (unless htey're both 'static) - even implicit drops have some order, so one item would be dropped before another.
Seems you are right.
The following code fails to compile, showing 'a has some relation with 'b
error[E0505]: cannot move out of s2 because it is borrowed
--> tests/test.rs:175:10
|
170 | let mut c2 = z(&s2);
| --- borrow of s2 occurs here
...
175 | drop(s2);
| ^^ move out of s2 occurs here
176 |
177 | println!("{}", c1.waker);
| -------- borrow later used here
The borrow checker can be quite clever at times in how it infers lifetimes.
Fortunately you don't have to understand how it works to write Rust code effectively. It helps, when you encounter problems, but at the end of the day the borrow checker is a tool to help you avoid lifetime errors. The error messages it gives are meant to point you in the direction of a lifetime error so that you can fix it.
In the earlier examples, there was no error, because the code was sound. It isn't terribly important how the borrow checker determines the code is sound, as long as it can determine the code is sound.
In your latest example, the code is unsound, because it's possible to redefine Context, z and drop such that each piece individually is fine but dropping s2 early would cause a use after free. (I'm pretty sure, anyway - I fiddled with it a bit, but the logic would be rather complex. As you have discovered, it can actually be quite difficult to make code break in exactly the way you want it to).
I find it useful when writing Rust code to focus on what the compiler says about my code, and not to try too hard to understand how the compiler itself reaches its conclusions. There is certainly merit to understanding how the borrow checker works but you don't need to completely understand the borrow checker to master borrowing itself.
That said, there's an interesting fact about this code, that actually implementing Drop for Contextalso causes it to fail, which is not unusual, but the error message confuses me because it seems to suggest that c1 can be dropped after passing to mem::drop. I think the logic that allows it to work without Drop should still apply with Drop, in this case, so maybe this is a borrow checker limitation, or perhaps I have overlooked something.