Why do Rust's stack and struct fields have opposite drop orders?

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!

https://rust-lang.github.io/rfcs/1857-stabilize-drop-order.html

10 Likes

Yup!

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.

3 Likes

I'm curious:

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?

1 Like

It's discussed in the Motivation section of the RFC that Alice linked.

1 Like

Yes, I read it. That's what prompted me to ask my questions. It does not really address them. Unless I missed a point somewhere.

1 Like

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.

4 Likes

OK. That makes a lot of sense to me.

Still leaves me wondering why the OP is concerned about this?

1 Like

This is the ancient pr that led the reverse drop order to be enforced on structs: should struct fields and array elements be dropped in reverse declaration order (a la C++) · Issue #744 · rust-lang/rfcs · GitHub

After a little bit of research, i think this is mainly for consistency, but it's tricky when it comes to self referencial structs ex:

struct SelfRef {
    pointer: *const String,
    data: ManuallyDrop<String>,
}

if data was dropped first, pointer would be a dangling pointer, so in self referential structures it's better to ensure the right order or implement your own Drop.
similar thing was discussed in rust internal forum Need for controlling drop order of fields - language design - Rust Internals .

Also notice how tuples are dropped in their order because "i think" they are simple and they don't have dependecies problems

check Destructors - The Rust Reference :

  • The fields of a struct are dropped in declaration order.
  • The fields of the active enum variant are dropped in declaration order.
  • The fields of a tuple are dropped in order.
  • The elements of an array or owned slice are dropped from the first element to the last.
  • The variables that a closure captures by move are dropped in an unspecified order.
  • Trait objects run the destructor of the underlying type.
  • Other types don’t result in any further drops.
5 Likes

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.

1 Like

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.

1 Like

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.

2 Likes

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.

1 Like

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

1 Like

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.

1 Like

Yes, it is unsafe code that can be impacted, as mentioned a couple times earlier.

1 Like

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

1 Like

I wish I was there to advocate deliberately randomizing field drop order.

2 Likes

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

1 Like

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.

1 Like