Why doesn't this compile?

Just an experiment to make sure that I am understanding the fundamentals properly. Looks like I am not...

I am trying to figure out why the following code does not compile. As I see it, in the function func(), param is passed by value. So, it owns the memory. So its lifetime is only the function, we should be able to make mref point to the local variable.

I am fine with an answer that this should have compiled (from the concept of lifetimes perspectives), but doesn't compile because of some internal implementation of the compiler. I am just trying to make sure that my understanding of lifetimes is correct.

#[derive(Debug)]
struct MyType <'a> {
    mval: i32,
    mref: &'a i32,
}

fn func(mut param: MyType) {
    let lcl = 50;
    param.mval = 100;
    param.mref = &lcl;
    println!("{lcl}");
}


fn main() {
    let tst = 20;
    let var = MyType { mval: 10, mref: &tst};
    func(var);
}

error[E0597]: `lcl` does not live long enough
  --> src\main.rs:11:18
   |
8  | fn func(mut param: MyType) {
   |         --------- has type `MyType<'1>`
9  |     let lcl = 50;
   |         --- binding `lcl` declared here
10 |     param.mval = 100;
11 |     param.mref = &lcl;
   |     -------------^^^^
   |     |            |
   |     |            borrowed value does not live long enough
   |     assignment requires that `lcl` is borrowed for `'1`
12 |     println!("{lcl}");
13 | }
   | - `lcl` dropped here while still borrowed

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

It has to do with drop scopes. Arguments to functions are dropped after any local variables declared inside the function. A simple modification like the following compiles.

fn func(param: MyType) {
    let mut param = param;
    let lcl = 50;
    param.mval = 100;
    param.mref = &lcl;
    println!("{lcl}");
}
2 Likes

Thanks.

These types have trivial destructors that don't require exclusivity at the end of the drop scope, so that's not it. (Additionally the workaround still "drops" param after lcl.)

When you call func, param has a lifetime that that caller chose -- one necessarily longer than the body of func. The workaround works because param is covariant in the lifetime, and when you make the assignment to a new variable, the compiler infers its type to have a shorter lifetime.

Another obscure workaround is to use a less trivial pattern in the signature:

-fn func(mut param: MyType<'_>) {
+fn func(mut param @ _: MyType<'_>) {

But effectively it too is invisibly assigning param to a new variable that can have a different type. See here and here.

7 Likes

Sorry, follow up question. Then how does this work? Looking at the link, lcl would be dropped before iref. That should have resulted in similar error.

fn func() {
  let iref: &i32;
  let lcl: i32 = 50;

  iref = &lcl;
  println!("{}", iref);
}

Because they have trivial destructors (no-ops); effectively the borrow ends immediately after the println as there are no further uses[1] requiring the lifetime to be alive, and lcl is thus free to go out of scope without causing a borrow error.


  1. such as a non-trivial destructor ↩ī¸Ž

1 Like

Uncomment the Drop implementation to see it act how you thought it would.

Oh my... I wasn't aware that this can make such a difference and I absolutely hate it :laughing:

4 Likes

Thank you for the responses. One more question:

#[derive(Debug)]
struct MyType <'a> {
    mval: i32,
    mref: &'a i32,
}

static gbl: i32 = 2;
static mut gmt: MyType = MyType {mval:1, mref: &gbl};


fn func(mut param: &mut MyType) {
    println!("{:?} in", param);
    param = &mut gmt;
}

Trying to compile this gives me:

error: lifetime may not live long enough
  --> src\main.rs:14:5
   |
12 | fn func(mut param: &mut MyType) {
   |         --------- has type `&mut MyType<'1>`
13 |     println!("{:?} in", param);
14 |     param = &mut gmt;
   |     ^^^^^^^^^^^^^^^^ assignment requires that `'1` must outlive `'static`
   |
   = note: requirement occurs because of a mutable reference to `MyType<'_>`
   = note: mutable references are invariant over their type parameter
   = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance

error[E0133]: use of mutable static is unsafe and requires unsafe function or block
  --> src\main.rs:14:13
   |
14 |     param = &mut gmt;
   |             ^^^^^^^^ use of mutable static
   |
   = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

I understand the second error. But, I don't understand the first error. Why should lifetime "1" outlive static? I looked at the given link. Don't quite follow how it is applicable here. Also, in the link there is this statement that is very confusing

we saw that it was not ok for us to treat &mut &'a U as a subtype of &mut &'b U, therefore we can say that &mut T is invariant over T

There is no "T" in the statement for us to say that "&mut T is invariant over T".

What am I missing?

Yes, sorry. I blindly fixed your snippet not having read the previous conversation.

When T is &'a U, &mut T is &mut &'a U.

MyType<'a> and MyType<'b> are different types and since &mut T is invariant over T, the compiler is not able to automatically adjust lifetimes so when trying to assign &mut MyType<'a> the 'a has to match exactly.

Elided lifetime in statics are 'static. So you're trying to assign a &mut MyType<'static> to a variable of type &mut MyType<'1>. The inner lifetime is invariant due to the &mut, which means the assignment can only be valid when '1 is 'static.

T is a placeholder for any type. That includes types with or without lifetime parameters, references, non-references, whatever.

2 Likes