Why Rust does not choose the minimal lifetime for variables?

For example, code:

#[derive(Debug)]
struct A(i32);

impl A {
    fn new(a: i32) -> A {
        println!("A::new({})", a);
        A(a)
    }
}

impl Drop for A {
    fn drop(&mut self) {
        println!("A::drop({})", self.0);
    }
}

fn main() {
    let x = A::new(0);
    
    let y = A::new(1);
    println!("{:?}", y);
    
    println!("{:?}", x);
}

This code gives output:

A::new(0)
A::new(1)
A(1)
A(0)
A::drop(1)
A::drop(0)

Why Rust does not drop the variable 'y' earlier, as showed on output below?

A::new(0)
A::new(1)
A(1)
A::drop(1)
A(0)
A::drop(0)

What is reason of this behavior of Rust's compiler?

This isn't really about lifetimes, but about destructors. The reason Rust always runs destructors at the end of the scope is so it is easy to reason about Rust code when you are reading it.

4 Likes

I have no idea about how compilers are constructed but my feeling is it's just the easier and most natural thing to get the compiler to do.

"Oh, a scope is ending now, I'll drop everything that I should now"

Must be easier than analyzing the code flow and figuring out the last time anything is used so as to drop it ahead of time. Imagine your code is full of conditions and loops for the compiler to analyse.

2 Likes

The compiler in fact does essentially that to better understand reference lifetimes: https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html#non-lexical-lifetimes

But for types that implement Drop it doesn't use that, because (as alice said) when there are meaningful side effects involved in dropping things -- as is common with scope guards, lock guards, ... -- it's considered more important to make it clear when dropping would happen than to allow more code to compile. (Not to mention that Rust 1.0 didn't, so it would have been a breaking change to existing rust code to change when they run.)

You can always call drop (it's in the prelude) if you want to be done with something early.

3 Likes

Generally, the compiler outputs code in the order you wrote it, unless it makes no observable difference.

In this case, it runs destructors in reverse order ( to the order in which the variables were declared ), which is what you would expect. It seems logical to me.

Whether the order is guaranteed, I am not sure. I wouldn't want to depend on the order in which destructors are called.

Edit: it seems the order is defined:

"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)."

Note that for structs, it works differently: " The fields of a struct are dropped in declaration order.".

https://doc.rust-lang.org/reference/destructors.html

AIUI this doesn't even have to be a special case for the borrow checker. That call to Drop::drop(&mut self) counts as a further use of the borrowed value, so NLL naturally includes that in the lifetime.

4 Likes

It's even more fun with a #[may_dangle]-dropping type like Vec:

fn main() {
    let mut x = 0;
    let mut v = vec![&mut x];
    *v[0] += 1;
    // NLL ends the borrowed lifetime here,
    // so we can access it directly again.
    x += 1; 
    dbg!(x);
    // v implicitly drops here! (with #[may_dangle])
    // but a manual `drop(v)` would extend NLL
}

So maybe it is a little special... :slightly_smiling_face:

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.