Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How are lifetimes of struct arguments treated in calls to functions with lifetime parameters?

Tags:

rust

This code passes the compiler (for clarification lifetimes are not elided):

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 to compile ("p does not live long enough")!

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'. True?

Even stranger (case 3): this only fails 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?

Cheers.

like image 403
dacker Avatar asked Sep 28 '22 19:09

dacker


1 Answers

Noting the difference with mut was a key observation. I think that it will make more sense if you change the type of the second argument and give one possible implementation:

fn test<'a, 'b>(_x: &'a mut Foo<'b>, _y: &'b i32) {
    _x._field = _y;
}

This function has the ability to mutate _x. That mutation also includes storing a new reference in _field. However, if we were able to store a reference that had a shorter lifetime (the intersection you mentioned), as soon as the inner block ended, the reference in the Foo would become invalid and we would have violated Rust's memory safety guarantees!

When you use an immutable reference, you don't have this danger, so the compiler allows it.

You have discovered an important thing - Rust doesn't always care what you do in the function. When checking if a function call is valid, only the type signature of the function is used.

I'm sure there's a fancy way of saying this using the proper terms like contravariance and covariance, but I don't know those well enough to use them properly! ^_^

like image 156
Shepmaster Avatar answered Oct 06 '22 02:10

Shepmaster