Rust's drop order vs common deconstruction orders

From the text in Drop Check document, it appears that the order of values being dropped in Rust does not depend on the order they are created, and therefore most of the document is explaining how this affects generics and the limits for those cases.

Something the document doesn't explain is why the order or dropping values is not set in a way to support more cases. In many languages, specially C++ and D, the order is almost guaranteed to be reverse of their definition order. From the arguments and examples, looks like that's not the case in Rust.

For example, in this sample:

struct Inspector<'a>(&'a u8);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("I was only {} days from retirement!", self.0);
    }
}

fn main() {
    let (inspector, days);
    days = Box::new(1);
    inspector = Inspector(&days);
    // Let's say `days` happens to get dropped first.
    // Then when Inspector is dropped, it will try to read free'd memory!
}

The comments at the end of main() function are considering if days gets dropped before inspector. If following the same rules as C++/D, the order will be inspector being dropped first, then days.

I know the document may be old, but I don't remember seeing more on this topic in other docs, either.

Any comments? Do you know why the things are the way they are? Or, do you know if anything has changed since that document was written/updated?

1 Like

Well, the playground gives an error message saying Rust uses the same drop order as C++, but this code still fails to compile:

error[E0597]: `days` does not live long enough
  --> src/main.rs:15:1
   |
12 |     inspector = Inspector(&days);
   |                            ---- borrow occurs here
...
15 | }
   | ^ `days` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

I believe the drop order is actually:

values in a scope are dropped in the opposite order they are declared

You can observe this by changing let (days, inspector); to let inspector; let days; and let days; let inspector;. The original fails to compile (because the drop order between two variables declared in the same let statement is undefined), the second also fails (because days is declared last, so must be dropped first), while the third works because inspector was declared last so is guaranteed to be dropped before days.

There's some mention of this in RFC 1857 which is stabilizing the order of dropping fields of structs.

[...] are treated as local variables (which are destroyed in LIFO order)

I can't find any RFC or any text in the reference specifying this behaviour, but there is a sentence in the book v1's chapter on Drop.

3 Likes

Then when Inspector is dropped, it will try to read free'd memory!

This won't happen, because references in Rust can't outlive the object they are referencing. This is enforced at compile time and you don't need to know the drop order to make it safe. It's safe by default.

1 Like

https://github.com/rust-lang/rfcs/blob/master/text/1857-stabilize-drop-order.md

is the canonical documentation on this.

If you click through to its PR, you can see a ton of discussion.

2 Likes

Thanks all for the insights and links! Very helpful! :slight_smile:

Well, this one is actually changing with non-lexical lifetimes, right? I mean, a value can reach it's end of life before the variable goes out of scope.

I suppose similar to that, the order of drops can be enhanced to allow compilation of the example I posted. But I think that's a question for after we have NLLs implemented.

NLL means the duration of a borrow can be refined, but it doesn't change the lifetime of actual values.

2 Likes

For instance, today you can't even simulate the actual drop order when things are borrowed:

fn main() {
    let x = vec![0];
    let y = &x;
    std::mem::drop(y);
    std::mem::drop(x);
}
error[E0505]: cannot move out of `x` because it is borrowed
 --> src/main.rs:5:20
  |
3 |     let y = &x;
  |              - borrow of `x` occurs here
4 |     std::mem::drop(y);
5 |     std::mem::drop(x);
  |                    ^ move out of `x` occurs here

You'd have to introduce a scope, for a lexical lifetime of the borrow. But with NLL, the borrow will properly end after that manual drop(y), and then x is free again.

4 Likes