Why could this work about lifetime?

lifetime 'a outlives 'b but the call violates the rule, and rustc no complains, why this work?

fn f<'a, 'b>(s: &'a i32, t: &'b i32) where 'a: 'b {
    println!("{} {}", s, t);
}

fn main() {
    let t0 = 3;
    let t = &t0;
    {
        let s0 = 2;
        let s = &s0;
        f(s, t);
    }
    println!("{}", t);
}

I think the way to read this is: can the compiler find 'a, 'b s.t.:

  1. the function declaration is satisfied

  2. the calling site is satisfied

  3. in particular, 'b does not have to be all of &t0; it can be a shorter lifetime

The way I see it t lives from the beginning of the main() scope until the end of main(). Meanwhile s lives from the beginning to the end of the inner scope.

The function f()is done before the end of the inner scope so both s and t live long enough to be borrowed by f().

The fact that the life time of t is longer than s (a outlives b) does not make any difference.

that's correct

that's incorrect.

fn main() {
    let t0 = 3;
    // let's call this lifetime '1
    let t = &t0;
    {
        let s0 = 2;
        // let's call this lifetime '2
        let s = &s0;
        // this call need to find some lifetime 'a and 'b, such that:
        // 'a: 'b; required by the function type signature
        // '2: 'a; required by assign argument `s` (lifetime '2) to parameter `s` (lifetime 'a)
        // '1: 'b; required by assign argument `t` (lifetime '1) to parameter `t` (lifetime 'b)
        // the above equations can be solved by both give 'a and 'b the temporary scope of the callsite
        f(s, t);
    }
    println!("{}", t);
}

'a: 'b simply means that &'a must be valid as long as &'b is valid. Which is true, &s is valid at least as long as &t is during the call. I think it is confusing to us because of the contra-variance of function parameters.

1 Like

Because of covariance of lifetime parameters. You are not calling

f::<'the_full_lifetime_of_s, 'the_full_lifetime_of_t>(s, t)

so to speak, instead the compiler infers way shorter lifetimes

f::<'super_short_lifetime1, 'super_short_lifetime2>(s, t)

which, for the sake of simplicity, may even be the same lifetime twice, as that would satisfy the 'a: 'b bound

f::<'super_short_lifetime, 'super_short_lifetime>(s, t)

Then, &'the_full_lifetime_of_s i32, the type of s, when passed to f can coerce into &'super_short_lifetime i32; the same thing is true for t.

I hope this makes sense – it’s a bit difficult to talk about because the lifetimes in question don’t actually have concrete names.


You can also see that the function does complain, when you rule out variance. One way to do this is to write

fn f<'a, 'b>(s: &mut &'a i32, t: &mut &'b i32) where 'a: 'b

Behind the mutable reference, lifetimes can no longer change, because this position is invariant. In other words, if you pass a mutable reference, the function f would also be allowed to write back to the variables referenced, so the lifetime in the argument type must match exactly the variable’s type in order to facility this bi-directional flow of information.

If you do this, you get the desired complaint from rustc:

fn f<'a, 'b>(s: &mut &'a i32, t: &mut &'b i32) where 'a: 'b {
    println!("{} {}", s, t);
}

fn main() {
    let t0 = 3;
    let mut t = &t0;
    {
        let s0 = 2;
        let mut s = &s0;
        f(&mut s, &mut t);
    }
    println!("{}", t);
}
error[E0597]: `s0` does not live long enough
  --> src/main.rs:10:21
   |
9  |         let s0 = 2;
   |             -- binding `s0` declared here
10 |         let mut s = &s0;
   |                     ^^^ borrowed value does not live long enough
11 |         f(&mut s, &mut t);
12 |     }
   |     - `s0` dropped here while still borrowed
13 |     println!("{}", t);
   |                    - borrow later used here

For more information about this error, try `rustc --explain E0597`.

The error message does not explicitly complain about the function call to f here, instead it assumes the 'a: 'b relationship desired by f is true, which limits the life of t not to go any further than s; and then goes from there, complaining about t being used in the println after it no longer must be used. Sure, removing the println! would “fix” the code, too; as would removing the call to f, ultimately the compiler chooses one possible point of conflict relatively arbitrarily, even when there’s multiple factors that all collude to produce the issue.

5 Likes

“Clarification” which may probably make some confused but helps me a lot. Feel free to ignore if you are Ok with just remembering rules about covariance, invariance and contravariance. I never is able to remember all these fancy words thus I needed different way to internalize these rules.

As I have already said I may never remember all these fancy words “covariance”, “contravariance”, etc. Let alone what's what and what is permitted when.

But I do remember that shared references are shared and unique references are unique.

That means that if someone have unique reference it must be passed around unmodified: there may only be one, lifetime is part of it's type and it must be unchanged thus everything works like @justinz expected if you pass around unique references.

Now, shared references, are, well… they are shared, wow! That means that compiler is free to make a different copy of them if it wants to do that. It needs to have the same lifetime or shorter lifetime. Original would still exist, original would still keep original variable pinned, everyone would be happy.

Why I'm talking about pinning here? Because reference, while it exists, pins the variable in memory. Like a bug pinned by a pin to the paper variable would stay in place as long as pin exists same is with variable (or value on heap, etc). That is why we couldn't change lifetime of unique reference: not only it gives mutable access to some variables inside of function f, but it also ensures that s0 and t0 are not moved (nomicon says every type must be ready for it to be blindly memcopied to somewhere else in memory, but for values rules are different: they may always be memcopied to somewhere else in memory only if there are no valid, live references which may be used to access them). And there may only be one.

Reference is a pin that keeps value in place and unique reference is unique while shared reference is not, you may have many pins that pin the same value in memory and that is why compiler can “play games” with refrences.

If references worked like @justinz expects the to work then original program would still be able to rewrite like this:

fn main() {
    let t0 = 3;
    let t = &t0;
    {
        let s0 = 2;
        let s = &s0;

        let s_prime: &i32 = s;
        let t_prime: &i32 = t;
        f(s_prime, t_prime);
    }
    println!("{}", t);
}

Code would still have worked, only developer would have been forced to do tedious copies of references when needed.

But transformation is mechanical and simple and, well, computer does “mechanical and simple” work better than human.

That is why covariance exist and why it only works with shared references, but not unique references.

Note that it is quite important to differentiate the lifetime of the reference from lifetimes in the refetent’s type.[1] The lifetime parameter of mutable references is covariant, too, like the lifetime of immutable references. Admitted, the notion of implicitly converting to shorter-lifetime borrows when passed to a function, whilst preserving the original reference, is slightly different, as for mutable references you need “reborrowing” whilst for immutable references, you can think of a copy plus a subtyping coercion; but still, making the reference mutable changes nothing in the original code:

// compiles perfectly fine

fn f<'a, 'b>(s: &'a mut i32, t: &'b mut i32) where 'a: 'b {
    println!("{} {}", s, t);
}

fn main() {
    let mut t0 = 3;
    let t = &mut t0;
    {
        let mut s0 = 2;
        let s = &mut s0;
        f(s, t);
    }
    println!("{}", t);
}

Only stuffing the lifetime of a reference behind another level of indirection which is mutable, finally makes things invariant. It’s &mut &'a i32 that makes 'a invariant, whereas &'a mut i32 would still be covariant with respect to 'a.


  1. I’m not entirely sure whether or not you are aware of this and trying to express this in your answer, but at least it’s not a nuance that’s clearly expressed. ↩︎

Yeah, indeed, I have forgotten about that part. It's another twinkle which needs an extra explanation, but goes to the same principles.

Here the idea is, essentially, that if you own you unique mutable reference you may still play games with it because you know no one else have any doppelgangers.

But when your mutable reference is borrowed, you can not do that: you only hold it (and can use it) for a limited time and have to return it in the “unbroken” state.

And one may invent many other relaxations of rules, ultimately it goes back to the Rice's theorem: no matter how many relaxations one would add to the Rust compiler there always would be some valid and correct program which would need still more relaxations.

That's the nature of the beast, but yeah, the number of these relaxations, in today's Rust is pretty large already.

Sorry to keep nit-picking, but one form or “borrowed mutable reference”, i.e. & &'a mut T is still covariant in 'a, whereas it’s a “mutably borrowed reference”, i.e. both of &mut &'a T or &mut &'a mut T that turn out with 'a invariant.

Of course that’s true; incidentally this thread demonstrates one (IMO minor, but still extant) disadvantage of making Rust’s compiler “smarter” here, as that can cause confusion of the form “why is this not rejected” from people who learn simplified versions of the full rule-set first. (And learning simplified rules is absolutely necessary to avoid having it too confusing or hard to learn.)

Ah… right in line with what I just wrote… On the other hand, I find it sometimes surprising how many use-cases of rules one doesn’t question. For example, re-borrowing is a thing that beginners don’t usually learn right away, and commonly get a bit surprised once they realize the first example of it happening – on the other hand, trivial stuff like

impl Foo {
    fn foo(&mut self) {
        self.bar();
        self.bar();
   }
   fn bar(&mut self) {}
}

would absolutely fail to compile without implicit re-borrowing, yet no beginner would ever question why this code works. (I suppose the magic of method notation does contribute to hiding the need for re-borrowing here as well though.)