Why can a reference with a shorter lifetime be saved in a struct and access in the drop?

struct A<'a>(&'a i32);
impl<'a> Drop for A<'a>{
    fn drop(& mut self){
        let _c = *self.0;
    }
}
fn main() {
  let a;
  let i = 0;
  a = A(&i);
  panic!();  // #1
}

This code can be compiled, however, according to drop-scopes

When control flow leaves a drop scope all variables associated to that scope are dropped in reverse order of declaration (for variables) or creation (for temporaries).

a is declared prior to i hence i should be first destroyed before a. At #1, the stack is unwinding, in which i is dropped before a, but as shown in the code, there is an access in the drop of a, at which time i has been dropped.

Why can this code be compiled? If I change the i to another type that implements Drop, the code won't code. Why implements Drop make them inconsistent?

3 Likes

the built in i32 type's drop glue is a no-op (i.e. empty).

the drop scope is a static concept, it's based on lexical scope, it's not checked at runtime (so unwinding the stack is irrelevant in this situation). in fact, you can replace the i32 with any type that doesn't have Drop implemented (i.e. all it's fields don't have Drop impls).

in the following snippets, if you compile with the no_drop (e.g. RUSTFLAGS="--cfg no_drop"), it will succeed. otherwise, you get a "reference not live long enough" error.

struct Foo;
#[cfg(not(no_drop))]
impl Drop for Foo {
    fn drop (&mut self) {}
}
struct A<'a>(&'a Foo);
// it only matter whether the type implements `Drop`
// you don't need to "access" whatever at runtime
impl<'a> Drop for A<'a>{
    fn drop(& mut self){}
}
fn main() {
  let a;
  let i = Foo;
  a = A(&i);
  // no need to unwind the stack to trigger the error
  // panic!();
}

the built in i32 type's drop glue is a no-op (i.e. empty).

no-op does not mean the object is live. If by your logical, the following code would be accepted:

fn main(){
   let r;
   {
     let i = 0;
     r = &i;
   }  // drop i here
   println!("{r}");
}

Since drop i is a no-op, what a big deal if we access i through r in the outermost block?

I think it's because i is on the stack in your inner scope there. When that scope is exited whatever space the occupied on the stack by i could be reused by something else. Which would mean the i is corrupted.

For example:

    let mut r;
    {
        let i = 0;
        r = &i;
    } // drop i here
    do_something();    // May scribble on the stack space previously used by `i`
    println!("{r}");

So, for my original example, since i is dropped prior to a, when a is dropping, the storage occupied by i can be reused or corrupted too. From this perspective, I don't think there is a difference between such two cases.

Unwinding-based borrow analysis takes scopes/drops into account differently than normal control flow it seems. If I had to guess, going out of scope is always a use but types with trivial destructors don't get used on unwind. Like there's no StorageDead for them or such.[1]

There's no chance of stack slot reuse ; you're either unwinding or terminating.


For those who missed it, this compiles

struct A<'a, T>(&'a T);
impl<'a, T> Drop for A<'a, T>{
    fn drop(& mut self){
    }
}
fn main() {
    let a;
    let i :
        i32             // No drop
    = <_>::default();
    a = A(&i);
    panic!();           // With Panic
}

But these don't.

    let a;
    let i :
        i32             // No drop
    = <_>::default();
    a = A(&i);          // No panic
    let a;
    let i :
        String          // Drop
    = <_>::default();
    a = A(&i);
    panic!();           // With panic
    let a;
    let i :
        String          // Drop
    = <_>::default();
    a = A(&i);          // No panic

  1. Just a guess. ↩ī¸Ž

4 Likes