"No Drop impl" marker trait?

Would it be possible to have a marker trait to indicate a type has no explicit Drop implementation?

There's a number of places where people use Copy as a proxy for this, but of course that also requires that the type be copyable (and cloneable). For example, in bytemuck.

That doesn't work when using a DST struct like:

struct Msg {
    len: usize,
    payload: [u8],
}

where &Msg would meet the requirements of most (all?) of bytemuck's traits (via a fat reference) except it can't be literally Copy or Clone. (This could be a way to represent a C structure with a flex-array, for example - playground link for more context.)

The Freeze trait covers "no interior mutability" aspect which people also use Copy for, so I guess I'm looking for the Drop-related equivalent.

There was some prior discussion which proposed as TrivialDrop marker trait, which I think is what I'm talking about here. But I couldn't find any RFC (active or otherwise) actually proposing this.

1 Like

Maybe take a look at std::mem::needs_drop(). It compiles down to a literal, so any branches are optimized out.

From the docs of needs_drop:

This is purely an optimization hint, and may be implemented conservatively: it may return true for types that don’t actually need to be dropped. As such always returning true would be a valid implementation of this function.

1 Like

That's interesting, but I'm really looking for a compile-time type assertion.

Just to clarify, do you mean that the type itself isn't Drop, or that nothing in it requires drop glue recursively?

Same guarantee as Copy - no part of the object needs code run when it gets destroyed.

If we ever get "where const" like functionality, this could be spelled as where const { !needs_drop::<T>() } / TBool<{!needs_drop::<T>()}>: IsTrue.

IIRC historically there's been some pushback on making a TrivialDrop trait since it morally shouldn't be breaking to add impl Drop, the same as for any other trait implementation, but with generic types this is more complicated due to NLL's interaction with drop glue.

3 Likes

needs_drop() is const, so theoretically you could cause a compilation error with it. However, it's allowed to have false positives (including returning true all the time), so it's a compatibility hazard.

2 Likes

IIRC historically there's been some pushback on making a TrivialDrop trait since it morally shouldn't be breaking to add impl Drop, the same as for any other trait implementation, but with generic types this is more complicated due to NLL's interaction with drop glue.

I guess, except that as soon as you add Drop you lose Copy so there's really no way that could ever be the case. TrivialDrop doesn't make this much worse.

Unlike Copy, I wouldn't expect TrivialDrop to be used all that much. I think it's really only going to be useful in these FFI-like circumstances where you need to pay close attention to precise representation details. I guess you could have a type which is !Copy + TrivialDrop which adding Drop would change the public API of, but that's precisely why one would want to use TrivialDrop in the first place.

Do you have any references to other discussions about this?

1 Like

Copy is opt in though. If your type wasn't copy before, it isn't a breaking change to add Drop. Most types in my libraries at least are not copy (they may be clone, maybe not).

For semver reasons this new trait should also be opt in.

It can be, more than other traits, because it changes when values of it release their borrows.

1 Like

That requires generic_const_exprs plus (currently entirely hypothetical) impl unification. So that's still ways off into the future.

The best options currently are approximating it via T: Copy or if needs_drop::<T>(). Both approaches are used in the standard library. I don't know any better ones.

1 Like

Then there really should be a way to specify that you cannot rely on the absence of certain traits on a type. Something like a non_exhaustive_traits attribute.

Currently you cannot rely on the absence of traits on a type, adding TrivialDrop would be the first negative trait afaik (if drop-glue auto-generated a Drop impl or something then this would be spelled !Drop which makes it much more obvious it is a negative trait).

How is TrivialDrop different from Copy here? If TrivialDrop were a plain marker trait (rather than an auto trait), wouldn't they be identical?

This could be codified with:

trait Copy: Clone + TrivialDrop {}

no?

Copy is optin, you have to implement it on your type, my assumption of TrivialDrop has always been that it's auto-generated. If it's a trait you still need to implement on your types as a way of stating that you will never add a Drop impl that's different. (And in fact is close to the idea of impl !Drop for Foo; being the only thing that can match a where Foo: !Drop, where negative bounds are only for types that have explicitly opted-out of ever implementing a trait, not just types that currently don't implement the trait).

2 Likes

I would expect it to be explicitly opt-in, so that it can be relied upon as a guarantee, while still being automatically checked. Thus there will be trivially droppable types which are not TrivialDrop.

I'd like to see more examples of !Copy types which should be TrivialDrop. One that seems to fit the bill are small iterator structs (like the various std Range types).

2 Likes

&mut T is a big example of TrivialDrop but not Copy.

ManuallyDrop<T> is another that's only conditionally Copy, but that's most likely to be seen in other types that do implement Drop manually.

2 Likes

Yes on requiring generic_const_exprs; "if we get where const" was intended to include implying some subset of that, as non-generic where const is completely useless (while trivially false bounds remain an error, as they are without feature(trivial_bounds)).

But for this specific case there's no need for any fancy symbolic unification. Even a version that only permits naming an associated const bool item (thus spelled const { T::NEEDS_DROP }) and does item unification would be sufficient (and amazing to have[1]).

(The post-monomorphization-error version of the constraint can already be written, FWIW. Inline const blocks will even make it trivial to write, although it's blocked on making monomorphized const evaluation errors no longer optimization dependent without the 10+ percentage penalty to perf that the initial PR has.)

For the automatic case, I personally prefer where const over T: TrivialDrop, as it makes the less-stable nature more evident. For the manual/derived case, I prefer TrivialDrop over !Drop — I'd prefer Drop as a trait bound to act as Destruct[2] instead (i.e. always hold) since there's real world desire for removing explicit Drop impls to be nonbreaking and !Drop has a fairly established informal meaning of linear (undroppable) types.


That "fickle consts" (those where the exact value is not part of the semver-stable API) are allowed to flow into the type system via const generics without any compiler assisted warning is perhaps an unfortunate exception to the usual strong expectation of "it compiles means it's stable," but it is the existing reality.

You don't even need to use const fn; char::UNICODE_VERSION is "fickle" by itself.


  1. The use of typenum illustrates that legitimately useful things are wanted and can be done even with essentially purely syntactical unification, usually by hiding the "what I need" compound bound behind a blanket implemented pseudo-alias trait. ↩︎

  2. It's not that farfetched of an idea to pretend Drop is blanket implemented for all types for the purpose of generic bounds and dyn Drop, imo, due to the already very magical nature of the trait. ↩︎

Yeah I was assuming for many impl Foo for T where const needs_drop::<T>() == false one needs a fallback impl where const needs_drop::<T>() == true and that those two Foo impls should then unify to get rid of the fickleness.

Otherwise you'd just use T: Copy because that's the only case that's guaranteed anyway.