I'm confused about the drop order in Rust. I understand that stack variables follow LIFO (Last-In-First-Out), but struct fields are dropped in declaration order (First-In-First-Out). Here's a minimal example to demonstrate:
struct A;
impl Drop for A {
fn drop(&mut self) {
println!("drop A");
}
}
struct B;
impl Drop for B {
fn drop(&mut self) {
println!("drop B");
}
}
struct C {
a: A,
b: B,
}
impl Drop for C {
fn drop(&mut self) {
println!("drop C");
}
}
struct D {
b: B,
a: A,
}
impl Drop for D {
fn drop(&mut self) {
println!("drop D");
}
}
#[test]
fn test_drop() {
let c = C { a: A, b: B };
let d = D { a: A, b: B };
}
The output is:
drop D
drop B
drop A
drop C
drop A
drop B
This seems counterintuitive to me. Why did Rust choose to implement different drop orders for stack variables and struct fields? Is there a specific safety or design consideration behind this decision?
I'd appreciate any insights into the rationale behind this design choice. Thanks!
Given a time machine we'd absolutely change it, but subtle ordering differences in unsafe code are the kind of thing that's super hard to detect breaking issues, so it's just not worth changing it.
Why do people want predictable drop order? A drop() method is just some code. What this is asking for is the ability to order code execution by carefully ordering members of a struct. This seems to me an exercise in obfuscation.
Surely having behaviour of you program dependent on drop order is a dependency between data items that should not be there or should be expressed in a more upfront way. No?
Why is random or unspecified drop order counterintuitive? If I give a bag of trash to the recycler I don't know or care to know what order the items in the bag are fed into the incinerator or whatever.
In short why want to make ones code behaviour depend on drop order?
It's not so much a matter of wanting your code to rely on drop order, but more that some existing code at the time of the decision did rely on drop order. Whether that happened deliberately or by happenstance is kind of a moot point— Altering the drop order would break the existing code in subtle, hard-to-debug ways for (relatively) minimal gain, and the backwards-compatibility argument won out.
Just wandering. Couldn't it be possible to change it with an edition? Imagine an attribute #[drop_order(fifo|lifo|default)] that can be put on structs to declare its fields drop order. In current edition default <=> fifo, and next edition could just swap it. And since editions are contained in a crate, there shouldn't be any problem, should it?
The only thing I can think of is that structs with public fields might be observed by some other crate, that assumes different drop order. So it might constitute a breaking change. But I'm not sure about that.
Has this idea been discussed before? I would love to read it, if it already has happened.
The issue I see with this is that it's hard to make an easy and automated migration. Should the migration automatically put #[drop_order(fifo)] over every struct? Almost nobody will want that though.
The 2024 migration did something almost this strong: if_let_rescope replaces many if let expressions with match and expects you to revert the ones that are unnecessary. I'm not sure whether that was a good idea, but if we see how people react to it, we can have some information about what effect that kind of noisy migration has.
That is exactly how I thought it could be done. Would it be convenient? Probably not. But on the other hand most code that doesn't deal with unsafe should be easy to migrate.
The other question is, is it necessary to do this for all structs that are being migrated. Does it matter if a struct has not a custom Drop implementation? If not, then this would be orders of magnitude less severe.
Yeah but arguably a bit of a pain due to then having to go back and removing all the annotations.
It only matters if any of the struct fields has drop glue. This doesn't really decreases the magnitude since it will still include any struct with e.g. Vec/String
Does is really? I thought this is like a tree falling in a forrest. If there is no one to observe struct being destructed, does it matter in which order drop glue for particular fields is executed? My intuition says no, although I would like to read an opinion of someone experienced with writing unsafe, on that matter.
For data which is uniquely owned, drop order doesn't semantically matter, and resource cleanup has no observable side effects. At worst maybe you suffer slightly worse TCO based on what call is the tail call. But although Rust encourages using such data, drop glue with side effects is also common, the canonical example being lock guards, but also including data structures where multiple fields reference the same backing data.
Usually it's possible and even better to define the structures such that field drop glue doesn't matter; the Drop::drop function necessarily runs before the field's drop glue, after all. But guaranteeing a defined deterministic order doesn't particularly cost anything, so it's preferable for the language to guarantee that execution order is consistently predictable, given the requisite domain knowledge.
Also, there's one specific scenario where field drop glue is preferable to manually ordering in Drop: in the face of unwinding. If you're cleaning up generic resources in Drop::drop, it's very easy to forget to run destructors for some of it if a destructor you call into panics. Generated drop glue handles this for you. (And the complexity of doing so correctly is part of the motivation behind considering making unwinding from Drop::drop into an aborting error condition.)
But there is someone, a field with drop glue can observe another being dropped.
Though I guess depending how you define "dropping" a field you may need two such fields. For example, does "dropping" a i32 field invalidate pointers to it in other fields? If it does then you only need one field with drop glue observing the i32 being dropped, otherwise you need two (the second being e.g. a Box<i32>, then the pointer to the i32 would surely be invalidated when the Box<i32> is dropped).
Subjectively, it was a mildly unpleasant pain in the ass that added little of value for the codebases I work on - but those are small codebases with single-digit author counts, and the total workload was on the order of minutes, so it wasn't a big deal and didn't discourage us from using the migration tool.