RustProgrammingRust Developer

Contrast the drop order guarantees between a struct undergoing total destruction via pattern matching versus one experiencing partial moves of individual fields.

Pass interviews with Hintsage AI assistant

Answer to the question.

History of the question: Early versions of Rust required explicit destructor calls. The introduction of the Drop trait automated resource cleanup but introduced complexity when combined with Rust's move semantics. The problem of partial moves—where some fields are moved out of a struct while others remain—required careful definition of drop order to prevent use-after-free or double-drop bugs. The language designers had to specify whether the custom Drop implementation runs in this scenario.

The problem: When a struct implements Drop, the compiler assumes the destructor needs access to all fields to maintain safety invariants (like unlocking a Mutex or freeing memory). If a pattern match moves only some fields (let Foo { a, .. } = foo), the remaining fields would need to be dropped, but the custom Drop implementation might access the moved fields, leading to undefined behavior. This creates a conflict between the programmer's intent to extract data and the type's guarantee that its destructor will run with full access to its internal state.

The solution: The compiler forbids partial moves of fields from a struct that implements Drop, unless the struct is completely deconstructed in the pattern (binding all fields). When totally destructured, the struct is considered moved, and Drop is not called; instead, individual fields are dropped in reverse declaration order. For types without Drop, partial moves are allowed because the compiler-generated drop code only touches the remaining fields.

struct NoDrop(String, i32); struct WithDrop(String, i32); impl Drop for WithDrop { fn drop(&mut self) { println!("Dropping: {}", self.0); } } fn main() { let no_drop = NoDrop("a".into(), 1); let NoDrop(s, _) = no_drop; // OK: partial move allowed // println!("{}", no_drop.0); // Error: value moved println!("Remaining: {}", no_drop.1); // OK: field 1 still valid drop(s); let with_drop = WithDrop("b".into(), 2); // let WithDrop(s, _) = with_drop; // Error: cannot partially move from type implementing Drop let WithDrop(s, n) = with_drop; // OK: total destruction, Drop is NOT called println!("Moved: {} and {}", s, n); // Fields dropped individually at end of scope }

Situation from life

A systems programming team built a Zero-Copy network packet parser. They defined a Packet struct holding a reference to a raw buffer and several metadata fields (timestamp, length). The Packet implemented Drop to return the buffer to a pool. They attempted to extract just the timestamp for logging while processing the packet later, using a partial move in a match arm.

Solution 1: Remove the Drop implementation and use a separate PacketHandle wrapper that manages the pool, while Packet becomes a plain view without drop logic. Pros: This allows partial moves of Packet fields and separates resource management from data access cleanly. Cons: It introduces an extra indirection layer and requires careful lifetime management to ensure the view does not outlive the buffer, potentially breaking safety if mismanaged.

Solution 2: Clone the timestamp field before the move to avoid partial move. Pros: This is a simple change that maintains the existing structure with minimal code churn. Cons: It incurs a runtime cost for cloning; while negligible for integers, it becomes significant for complex metadata, and it fails to address the underlying architectural constraint of the type system.

Solution 3: Restructure the processing function to take ownership of the entire Packet, extract fields via total destruction, and reconstruct a new Packet if needed for the pool return. Pros: This works strictly within Rust's safety guarantees and makes ownership transfer explicit. Cons: It is verbose and requires careful handling to ensure the buffer is returned properly; failure to reconstruct correctly could lead to resource leaks.

The team selected Solution 1 because it fundamentally aligned with Rust's ownership model by decoupling the resource (the buffer) from the view (the metadata). This eliminated the compilation errors immediately, improved code clarity by distinguishing between resource management and data viewing, and maintained the zero-cost abstraction requirements of the project.

What candidates often miss

Why does the compiler forbid partial moves on types implementing Drop?

When a type implements Drop, the compiler generates a call to drop() at the end of the scope. The drop() method receives &mut self, implying it requires access to the entire struct to maintain safety invariants like releasing locks or freeing memory. If a field were moved out earlier via partial move, drop() would attempt to access freed memory or invalid resources, causing undefined behavior. By requiring total destruction (binding all fields), Rust ensures the destructor code is never executed; instead, fields are dropped individually, bypassing the potentially unsafe custom logic.

What is the exact drop order when a struct is totally destructured via pattern matching?

When a struct is fully deconstructed (e.g., let MyStruct { field1, field2 } = my_struct;), the struct's Drop implementation is suppressed entirely. The fields are then dropped in reverse order of their declaration in the struct definition (field2 then field1 in this case). This behavior matches the standard drop order for struct fields, but critically skips the container's custom destructor, preventing it from observing the moved-out state and violating safety guarantees.

Can a type with Drop be Copy if we ensure the destructor is idempotent?

No, the Rust compiler enforces that Copy and Drop are mutually exclusive via trait coherence rules, regardless of the destructor's actual implementation. This is a deliberate conservative design choice: even if drop() is currently empty or idempotent, allowing Copy would permit implicit bitwise duplication. Future modifications might render drop() non-idempotent, silently breaking safety guarantees, and since the compiler cannot verify idempotency in the general case at compile time, it outright prohibits the combination to prevent unsoundness.