What is effect of the " forcing the lifetime to be invariant"?

In this first example I think NLL just shortened the lifetime of c1 to the end of the inner block. Here's what happens when you force it to last longer.

1 Like

Seems life time invariant take effect only in generic.

#[test]
fn e() {
    pub struct Context<'a> {
        waker: &'a String,
        _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
        };
        // this is OK
        c2 = c1;
    }

    // this fails to compile
    fn wrapper<'a: 'b, 'b> (mut c1: Context<'a>, mut c2: Context<'b>) {
        c2 = c1;
    }
}

Yes, but it should not compiles if the life time invariant takes into effect even NLL.

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.

because life time invariant force 'a = 'b fails even 'b when is longer

In the unquoted portion of my post, you'll see there's only one lifetime, 'a.

Lifetimes only look forward in execution, so it doesn't matter that 'a exists before the inner block, in case that's the confusion.

Consider the following code:
Context.f is invariant.
And obviously c1::<'a>::f life time 'a not equal to c2::<'b>

But why c2.f = c1.f is allowed?

#[test]
fn e() {
    pub struct Context<'a> {
        waker: &'a String,
        f: *mut fn(&'a ()) -> &'a (),
    }

    let mut s1 = "1".to_owned();
    let mut c1 = z(&s1);

    let mut s2 = "2".to_owned();
    let mut c2 = z(&s2);
    c2.f = c1.f;

    println!("{}, {}", c1.waker, c2.waker);
    drop(c1);
    drop(c2);

    fn z<'a>(s: &'a String) -> Context::<'a> {
        Context {
            waker: &s,
            f: 0 as *mut fn(&'a()) -> &'a (),
        }
    }
}

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 T lives for": it means "the duration of time I am borrowing this T for", which may be shorter.

See the above line.
You can never say c1 has a same life time with c2

Can printing c2 after dropping c1 make the life time different? And the code compiles

#[test]
fn e() {
    pub struct Context<'a> {
        waker: &'a String,
        f: *mut fn(&'a ()) -> &'a (),
    }

    let mut s1 = "1".to_owned();
    let mut c1 = z(&s1);


    let mut s2 = "2".to_owned();
    let mut c2 = z(&s2);
    c2.f = c1.f;

    println!("{}, {}", c1.waker, c2.waker);
    drop(c1);

    println!("{}", c2.waker);
    drop(c2);

    fn z<'a>(s: &'a String) -> Context::<'a> {
        Context {
            waker: &s,
            f: 0 as *mut fn(&'a()) -> &'a (),
        }
    }
}

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.

By the way, &T is Copy, so dropping it is a no-op - drop receives the copy, and original reference is still here.

Seems you don't catch my point?
I am talking about invariant, e.g. *mut fn(&'a ()) -> &'a () ====> *mut T

means
c2.f = c1.f;
will fail to compile

'a T U
* &'a T covariant covariant
* &'a mut T covariant invariant
* Box<T> covariant
Vec<T> covariant
* UnsafeCell<T> invariant
Cell<T> invariant
* fn(T) -> U contra variant covariant
*const T covariant
*mut T invariant

If you were right, this would always fail to compile, unless both c1.f and c2.f are 'static. This would be rather inconvinient, wouldn't it?

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

#[test]
fn e() {
    pub struct Context<'a> {
        waker: &'a String,
        f: *mut fn(&'a ()) -> &'a (),
    }

    let mut s1 = "1".to_owned();
    let mut c1 = z(&s1);

    let mut s2 = "2".to_owned();
    let mut c2 = z(&s2);
    c2.f = c1.f;

    println!("{}", c2.waker);
    drop(c2);
    drop(s2);

    println!("{}", c1.waker);

    fn z<'a>(s: &'a String) -> Context<'a> {
        Context {
            waker: &s,
            f: 0 as *mut fn(&'a ()) -> &'a (),
        }
    }
}

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 Context also 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.

1 Like

@zylthinking if you want to play with lifetimes, I recommend that you use function boundaries, which are way more precise / under your control than using a borrow, whose lifetime can be shrunk before assigning the borrow to an invariant structure. Otherwise you will be testing invariance with the attempts from the compiler to let your code compile nonetheless.

Basic example:

struct Context<'a> {
    waker: &'a String,
    f: *mut fn(&'a ()) -> &'a (),
}

fn with_lifetimes<'_1, '_2> (
    c1: Context<'_1>,
    c2: Context<'_2>,
)
// where '_1 : '_2, /* if you want to add a `_1 ≥ _2` constraint. */
{
    {c1}.f = c2.f; // Error
}
3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.