[Solved:] How are lifetimes of struct arguments treated in calls to functions with lifetime parameters?

(simplified example and solution 8 posts down)

Hi all, regarding lifetimes:

The following code compiles fine:

struct Foo;

struct Link<'a> {
    _link: &'a Foo,
}

fn store<'a>(_x: &Link<'a>, _y: &'a Foo) {
}

fn main() {
    let a = Foo;
    let x = &mut Link { _link: &a };
    {
        let b = Foo;
        store(x, &b);
    }
}

But if I change store's definition to take a mutable reference to a Link:

fn store<'a>(_x: &mut Link<'a>, _y: &'a Foo) {
}

I get a compile error 'b does not live long enough'.

In case of an immutable reference, Rust does not complain about b going out of scope before x. In case of a mutable reference, it does.

What gives?

Does store preemptively "refuse" to take a &mut, because it "might" mutate the reference inside the Link (to &b, which has insufficient lifetime), even though it doesn't?

1 Like

You need to give another lifetime to the y: &Foo:

fn store<'a, 'b>(x: &mut Link<'a>, y: &'b Foo) {

This is because b doesn't live long enough (it gets out of scope soon) and you need a lifetime as long as of the constant a (which b hasn't).

Thanks for the answer, Thelost!

That absolutely makes it work with &mut.

However, beyond that, I was trying to get a deeper understanding of lifetimes and borrows in Rust, and so would really be interested if someone could point me to a formal explanation why the code works with a & but not with a &mut (in both cases b goes out of scope before x)?

Think: you got a reference to something mutable, then the value goes out (die), and then you try to mutate the value by your reference. It's called "use after free", and it's something Rust avoids.

When you tried to compile the code with an immutable reference (and you didn't try to access the value, by the way), it gone fine. But, once you have tried to get some mutable reference (even though you haven't changed or accessed the value), you got something bad, because you've given a lifetime that doesn't live long enough, then, your mutable reference wasn't secure.

Well, not quite, because moving the lifetime parameter from the Link to the &mut compiles fine again:

fn store<'a>(_x: &'a mut Link, _y: &'a Foo) {
}

Also, as before, just taking a mutable reference of *x (funny: x is not moved into store as I first thought), doesn't give you "something bad", because as you can see from this example, it doesn't violate any rules of the borrow checker.

It must have to do with how 'a is "filled in" when calling store:

In case of

fn store<'a>(_x: &mut Link<'a>, _y: &'a Foo) {
}

'a seems to be forced to local variable x's lifetime, which is too long for b, hence 'b does not live long enough'.

In case of

fn store<'a>(_x: &'a mut Link, _y: &'a Foo) {
}

and

fn store<'a>(_x: &Link<'a>, _y: &'a Foo) {
}

'a seems to be set to the intersection of the lifetimes of the two references which is b's lifetime, and everything is fine, as expected.

I guess what I'm looking for is an explanation of the different impact of the &mut Link<'a> part compared to &'a mut Link on establishing the actually used 'a.

Cheers.

(Changed x, y and link to _x, _y and _link to eliminate compiler warnings about unused variables, Thelost's code quote is correct.)

1 Like

It will be easier to reason about the rules if you avoid relying on lifetime elision. When you don't put explicit lifetimes in the signature, you get extra lifetime parameters, so actually

fn store<'a>(_x: &'a mut Link, _y: &'a Foo) is
fn store<'a, 'e>(_x: &'a mut Link<'e>, _y: &'a Foo)

fn store<'a>(_x: &mut Link<'a>, _y: &'a Foo) is
fn store<'a, 'e>(_x: &'e mut Link<'a>, _y: &'a Foo)

fn store<'a>(_x: &Link<'a>, _y: &'a Foo) is
fn store<'a, 'e>(_x: &'e Link<'a>, _y: &'a Foo)

Now, why &'a Link<'a> is treated differently from &'a mut Link<'a> when 'a is shorter than the Link instance's actual lifetime parameter, is an interesting question.

2 Likes

Exactly, you got that.

Thanks for the clarification, gkoz.

It seems the 'a in Something<'a> is treated fundamentally different from the 'a in &'a Something when determining actual lifetime arguments.

I reduced the example to boil down the problem and followed gkoz' suggestion to not elide lifetimes.

This code passes the compiler:

struct Foo<'a> {
    _field: &'a i32,
}

fn test<'a, 'b, 'c>(_x: &'a mut Foo<'c>, _y: &'b bool) {  // case 1
}

fn main() {
    let f = &mut Foo { _field: &0 };
    {
        let p = false;
        test(f, &p);
    }
}

If I use 'b instead of 'c in test's definition like so:

fn test<'a, 'b>(_x: &'a mut Foo<'b>, _y: &'b bool) {  // case 2
}

the code fails.

What I would expect to happen at the call of test in case 2 is:

  • 'a is set to the actual lifetime of f,
  • 'b is set to the intersection of the Foo's actual lifetime and &p's actual lifetime which is &p's lifetime,

and everything should be fine, as in case 1.

Instead, what actually seems to happen in case 2 is that 'b is forced to become the lifetime of the Foo which is too big for &p's lifetime, hence the compiler error 'p does not live long enough'.

Even stranger (case 3): this is only true if test takes a &mut. If I leave the <'b> in, but remove the mut like so:

fn test<'a, 'b>(_x: &'a Foo<'b>, _y: &'b bool) {  // case 3
}

the code passes again.

Anyone to shed light on this?

I think this is because compiler only allows shortening of struct's lifetime for immutable borrow. It is safe, since immutable borrow only allows reading. However, mutable borrow also allows writing, and if struct's lifetime was shortened, this would allow to assign a value to field that does not live as long as the struct itself, causing use after free.

3 Likes

Great answer, mkrasnenkov (I would mark it if I could).

That clears all 3 cases for me now.

In case 1, because of the &mut, 'c gets Foo's original lifetime. 'b is independent and does no harm.

In case 2, again because of the &mut, 'b gets Foo's original lifetime. This time, the &p is "too short" to fit into the &'b bool, hence 'p does not live long enough'.

In case 3, 'b is allowed to be shortened to &p's lifetime because there is no danger of test mutating _field to a reference that is "shorter" than the original Foo.

Hope, I understood correctly.

Even though it seems obvious now, do you happen to know where to read about 'no shortening of a struct's lifetime for mutable borrows'?

Thx a lot, you made my day!

I don't know for sure, but I think technical term for this is lifetime parameter variance. Sorry I can't provide any direct links (I'm on mobile).

I don't think I've read about this particular case though. But it is not hard to understand what borrow checker is doing, since the rules it is built upon are fairly logical.

For example, in this case they are:

  1. When reading, lifetime of a value you get from struct can be assumed shorter.
  2. When writing, lifetime of a value you set to struct must live at least as long as the struct itself.

A read-only reference to struct is coercible to reference with shorter lifetime, it is contravariant over lifetime.
A write-only reference to struct is coercible to reference with longer lifetime, it is covariant over lifetime.
A read-write reference can't be coerced, since it will make either operation unsafe. It is invariant.

I may be wrong what is co- and what is contra, but the general idea should be correct.

4 Likes

This was cross-posted to Stack Overflow.

You are right, mkrasnenkov. It is logical and follows from the basic borrow checker rules. Actually no need to read up on any special rules.