RFA-412 · Case file with fixtures · Case 384 of 694 · Runtime evidence
needs_drop Is False for ManuallyDrop of a Drop Type
needs_drop asks whether the queried outer type requires compiler-run drop glue. ManuallyDrop suppresses that automatic destruction, so its result is false while the programmer still owns an explicit and unsafe cleanup obligation.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- ManuallyDrop suppresses automatic drop glue for its inner value, and needs_drop describes compiler-run destruction for the queried outer type rather than resources hidden inside it.
- First discriminating check
- Query the exact outer type and audit the explicit initialization-state protocol that decides whether ManuallyDrop::drop must be called.
I saw a type containing a resource with Drop and expected needs_drop to report true. The resource was wrapped in ManuallyDrop, so the outer type deliberately had no automatic destructor work.
The failing fixture checks needs_drop::<ManuallyDrop<Tracked>>(). Rust returns false even though Tracked itself implements Drop.
The query is about the exact type
std::mem::needs_drop asks whether dropping values of T requires drop glue. The type parameter in this case is not Tracked; it is ManuallyDrop<Tracked>.
ManuallyDrop has one central job: prevent Rust from automatically running the inner value's destructor. The wrapper has the same layout as its inner value, but its lifecycle contract changes.
Therefore these two queries can differ:
assert!(std::mem::needs_drop::<Tracked>());
assert!(!std::mem::needs_drop::<std::mem::ManuallyDrop<Tracked>>());
The result describes compiler-managed destruction at that layer.
False does not mean no resource exists
The wrapped value can still own memory, a file, a lock, or another external resource. needs_drop == false does not prove that forgetting the value matches application policy. It says Rust will not automatically invoke drop glue for the queried type.
With ManuallyDrop, the programmer accepted responsibility for deciding whether and when the inner destructor runs. That responsibility is invisible to a generic automatic-drop query by design.
I avoid translating needs_drop into the phrase “contains nothing to clean up.” The stronger phrase is “requires no automatic drop glue for this type.”
Explicit destruction is unsafe because state must be proven
The repaired fixture constructs one tracked value and calls ManuallyDrop::drop. The call is unsafe because the caller must know that the inner value is currently initialized and has not already been dropped.
After the call, the wrapper's bytes must not be exposed as a live Tracked. Calling drop twice would destroy one value twice. Forgetting the call leaks the resource. Dropping uninitialized bytes is also invalid.
The real invariant is a state machine:
uninitialized -> live -> manually dropped
Only the live state permits exactly one explicit destruction.
needs_drop is an optimization hint with a one-way guarantee
The documentation allows needs_drop to conservatively return true in cases where no destructor would ultimately run. A false result is the useful guarantee: dropping T has no side effects requiring drop glue.
I do not use a true result as proof that a custom Drop method exists. Types can require recursive field destruction or be treated conservatively.
Most ordinary code should simply let Rust drop values. The query is useful in low-level collection implementations that may skip iteration when element destruction is definitely unnecessary.
It is rarely a reason to add unsafe lifecycle code to application logic.
ManuallyDrop should not be a public footgun
Public structures with accessible ManuallyDrop<T> fields can let safe callers observe or move bytes after an unsafe internal drop. Generic derives may also read fields in ways incompatible with a manually destroyed state.
I keep the wrapper private, store an explicit state when needed, and expose safe operations that preserve the state machine. I review trait derivations such as Debug, Clone, and equality instead of assuming layout transparency implies behavioral transparency.
The standard documentation contains important warnings around generic parameters and moving a manually dropped value. I treat those warnings as part of the API contract, not edge trivia.
Often Option or ordinary ownership is safer
If I need to take a value once, Option<T> often models the state directly:
let value = slot.take();
Some means live and None means absent. Rust then handles destruction of the remaining live case automatically.
ManuallyDrop is appropriate for low-level unions, specialized containers, and carefully proven destruction ordering. It is not a faster general replacement for Option without measurement and a complete unsafe proof.
Panic paths are part of the state machine
If code changes its state flag before manual destruction and the destructor panics, or constructs a replacement between transitions, unwinding can expose inconsistent state.
I order state changes so every panic point leaves a representation that later cleanup understands. For resources with fallible close, I use an explicit finalization method because Drop cannot return the error.
The fixture uses a simple counter and no panic so it isolates the automatic-versus-manual property.
My audit questions
When needs_drop and intuition disagree, I ask:
- What exact outer type is queried?
- Which wrapper suppresses or owns destruction?
- Who proves initialization state?
- Can cleanup occur exactly once on every exit path?
- Can safe public code observe a post-drop value?
- Would Option or a separate owner type encode the state safely?
The core principle is that a resource and its automatic drop glue are different facts. ManuallyDrop<Tracked> still contains the tracked resource bytes, but Rust has been told not to destroy them automatically. needs_drop correctly reports that outer compiler contract as false, leaving explicit cleanup to the unsafe protocol.