Drop bounds are useless, so let's change that

Bounding a generic type on Drop is essentially useless, to the point there's even a default warn lint against it:

warning: bounds on `T: Drop` are most likely incorrect, consider instead using `std::mem::needs_drop` to detect whether a type can be trivially dropped
 --> src/lib.rs:1:13
  |
1 | pub fn f<T: Drop>(_: T) {}
  |             ^^^^
  |
  = note: `#[warn(drop_bounds)]` on by default

A trait bound on Drop requires that an explicit impl of Drop for the type is present. That this is writable is actually quite awkward for API stability as well — generally people want to consider replacing a Drop implementation with drop glue that accomplishes the same cleanup to be a compatible change, but explicit bounds on Drop mean that removing an impl is API breaking, and it doesn't fall under any category of allowed minor name resolution breakage.

On the other hand, an empty trait that is blanket implemented for all types (e.g. Destruct) can actually be useful in some cases. Potential applications include:

  • dyn Destruct as a maximally erased trait object where you have the vtable entries for drop_in_place, size_of_val, and align_of_val but nothing else. Like using dyn Any for when you want to hold ownership of something but don't need the ability to downcast and/or want to avoid Any's 'static requirement.
  • ~const Destruct for expressing ~const bounds on drop glue.
  • As part of an unbound hierarchy[1] where a bound on Destruct removes the default Sized bound and only provides the ability to drop_in_place.

I'd like to propose that we just forbid writing trait bounds on Drop over an edition boundary with the hope of eventually making Drop as a trait bound actually mean "can be dropped." Doing this would have the added benefit of making !Drop actually mean something closer to linear types like that syntax gets informally used instead of "doesn't have a Drop impl but could still have nontrivial drop glue."

The only thing that really suffers from such a change IMHO is that Drop and Copy (the trait bounds) aren't mutually exclusive anymore. What people actually mean when they say they're mutually exclusive still holds — Copy and nontrivial drop glue are still mutually exclusive — but it does become possible to have types that are Copy + Destruct.


  1. The "less than Sized" capabilities/traits:

    • Pointee: can be the target of a pointer, can be by-value function parameter (potentially unsized)
    • Destruct: can ptr::drop_in_place
    • DynAligned: can mem::align_of_val (may require reading from the value)
    • DynSized: can mem::size_of_val (may require reading from the value)
    • MetaAligned: can determine required alignment from just wide pointer metadata, can be a dynamically sized struct tail
    • MetaSized: can determine size from just wide pointer metadata
    • Aligned: can mem::align_of, can offset_of! to a field of this type
    • Sized: can mem::size_of, can offset_of! over a field of this type
    • Relocate: can be trivially relocated to different addresses by just ptr::copy (while unpinned)
    • I feel like I've definitely missed some
    ↩ī¸Ž
7 Likes

What is the use case?

You could always call drop(foo) no matter whether Foo: Drop holds.

Currently, the bound just tell you "What you can do", but Drop is a negative bound, with Drop, you could only do less thing (for example, partial move).

Copy means you could directly use memcpy to create a new clone, Drop means you must call the drop guard when variable out of scope. It could be a terrible idea allowing Drop+Copy

There's a lint already, right? But even then, forbidding it altogether probably makes sense too. I don't think it's a good idea to ever allow them with changed semantics, that seems potentially confusing.

1 Like

When I implemented this lint, I found out about this:

Ah, yes, pin projection actually does need to reject a manual Drop implementation but is fine with the fields having drop glue of their own. Without an alternative solution, making drop bounds a hard error (or changing their meaning) isn't doable.

Personally, I conceptualize pin projection as always adding a Drop which shims to a pinned drop signature. This is simpler than provoking impl Drop to cause an error (as long as writing an empty pinned drop impl isn't necessary), but it does impact lifetime capture behavior when there are generics without drop glue[1].

There is an obvious alternative, though: allow the pin projection macro to just directly write what it wants: impl !Drop for Self. The full extent of negative impls' implications (e.g. on coherence) aren't fully clear yet (let alone fully implemented), but they are fundamentally required to exist since impl !DerefMut for &_ and impl !Clone for &mut _ are required to exist for soundness.

...but this has the same issue that Drop bounds currently have: !Drop looks like "cannot be dropped" instead of "only has automatic drop glue" which is what it actually means. Preventing the Drop impl using a negative impl is still preferable but surfaces the same confusion of Drop only being a portion of the type's drop glue and that types can have nontrivial drop glue without implementing Drop.

I want to be able to conceptualize the Drop trait as providing drop_in_place and Drop::drop just being a convenient way to compose its definition. This works by conceptualizing Drop has having a default implementation overridden by an explicit one. Unfortunately the need of library pin projection to interact with this "shallow" portion of the drop glue for soundness seems to make this impossible.

So Drop (has "whole item" drop glue in addition to its fields') and Destruct (can be dropped) are required to stay as separate concepts. I'm obviously not particularly fond of this reality -- the lint's existence indicates that this is known to be confusing -- but can at least accept that it's neccessary.

While I don't disagree, I thought of it as potentially acceptable since Drop bounds in the current editions are basically always in error and shouldn't exist (with the one exception of emulating a negative impl). So you could just ignore the old semantics and use the new ones, relying on warn(drop_bounds) to warn you when the bound isn't writable.

No it doesn't; that's the core thesis of this thread. If I write struct S(String);, S does not implement Drop (the bound S: Drop is not satisfied) but it still has nontrivial drop glue (needs_drop::<S>() is true). I don't even need to define a custom S for my example, as this is the case for String already:

error[E0277]: the trait bound `String: Drop` is not satisfied
 --> src/main.rs:5:18
  |
5 |     bounded_drop(s);
  |     ------------ ^ the trait `Drop` is not implemented for `String`
  |     |
  |     required by a bound introduced by this call
  |
note: required by a bound in `bounded_drop`
 --> src/main.rs:1:20
  |
1 | fn bounded_drop<T: Drop>(_: T) {}
  |                    ^^^^ required by this bound in `bounded_drop`

In the world described in the OP, Drop would be an always-satisfied trait bound (up until support for exotically unsized types exist, anyway). T: Copy would still imply that the type has trivial drop glue (that drop::<T> and forget::<T> are identical functions) and prevent a manual implementation of Drop; the only change would be to make a T: Drop bound mean "T can be dropped" instead of "dropping T requires more than just dropping its fields individually." Using Drop as a trait bound doesn't add capabilities, it takes them away (although those removed couldn't be used in a generic context anyway).


  1. With a Drop implementation, self is "used" at drop timing. Without a Drop implementation, self isn't, but the fields of self are individually iff they have drop glue. This means that any captured lifetimes within a bound generic are known to only be touched at drop timing by that generic type's own drop glue, and in the absense of that, the region is not required to be live at drop timing. This is NLL for user-defined types without the unsafe #[may_dangle] attribute. ↩ī¸Ž

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.