[Feature request] is_bitwise_copyable<T>()

Consider adding is_bitwise_copyable<T:Clone>() which return false if cloning values of type T matters.

Something similar could be achieved with impl specializations, by checking does T implements Copy. But that's not the same - Clone -able type may be bitwise-copyable too!

1 Like

Isn't the standard way for a type to say "I can be bitwise copied" is for it to impl Copy? If such a type didn't implement copy, how would it signal to is_bitwise_copyable that it is, in fact, bitwise copyable?

9 Likes

Maybe, 1GB of data is bitwise copyable, but we may never copy the whole data.

No; it's possible that a type implements Copy, but its Clone impl still does funky things (like printing a message to stdout).

But, the compiler knows whether a given type derives Clone (and as such doesn't do odd things), or if it does implement Clone manually (and thus might or might not do odd things).

Some containers in stdlib, like Vec, have the "optimization" that uses Copy instead of Clone whenever applicable. This could theoretically break buggy code, but the intent of Clone has always been that if it breaks it's because the Clone impl is bad, not because Vec itself is bad.

So maybe the compiler could provide a special macro-like primitive derived!(impl Trait for Type), that works for all traits for which Rust provides a #[derive], and returns true if the impl was generated by a #[derive], returns false if it was written by hand, and emits a compiler error if the impl doesn't even exist.

If this existed, it could be possible to forgo calling Clone only if it was automatically derived. There may be interesting cases where it's important to know whether PartialEq, PartialOrd, Eq and Ord were automatically derived, as well.

However, it doesn't solve everything because the type may be a struct that contains, inside it, types that had custom impls.

A single derive macro can implement multiple traits, one of which could be used to mark types that used the macro (although types could lie by implementing such a marker trait manually). The standard library does this:

Such a derived! macro presumably would have the advantage of not letting types lie.

Sorry, I am still not understanding your use-case for is_bitwise_copyable. What's your use-case for this function? It kind of sounds like you want to know if a Clone impl is going to have side-effects, but I don't see how is_bitwise_copyable helps with that.

I suppose such a function could be useful to know if a non-Copy type is safe to use with std::intrinsics::copy.

How would rustc know that a non-Copy type is safe to use with memcpy/ptr::copy? The literal purpose of Copy is to indicate "safe to use with memcpy`.

3 Likes

I want to know, should I call type's clone, or copy_nonoverlapping is enough.

Concrete example here https://github.com/tower120/any_vec/blob/main/src/clone_type.rs#L22-L28 . AnyVec is type erased Vec, and I store it's clone function. As optimization I want to eliminate clone_fn call, If type is Copy-able. The problem is that impls! is actually not able to check if type is copyable in generic context.

I assume, that in general, one ptr::copy_nonoverlapping over large chunk of memory is faster then multiple clone calls, even when types are known compile-time. (For my untyped case this is definitely true).

How would rustc know that a non-Copy type is safe to use with memcpy/ptr::copy?

I imagine in similar way as cpp do std::is_trivially_copyable, or as rust check if type Copy-compatible. Namely - empty Drop::drop, copy-compatible struct fields, etc...

Namely - empty Drop::drop, copy-compatible struct fields, etc...

That might not be quite sufficient. For example, perhaps a type which is Clone assigns a new unique id to each instance at the moment of cloning, for some reason — the internal id is Copy but shouldn't be copied.

I'd propose it should be defined as: may return true if the type is known to implement Copy, or if it has a derived Clone impl.

(However, this might run into the same soundness issues as specialization, and if we have specialization we don't need it.)

For example, perhaps a type which is Clone assigns a new unique id to each instance at the moment of cloning

Can't rust check for side effects?

I'd propose it should be defined as: may return true if the type is known to implement Copy, or if it has a derived Clone impl.

Why may? There could be false negatives with detecting if type Copy-able? Otherwise sounds good enough to me.

(However, this might run into the same soundness issues as specialization, ..

Wait a minute - but need_drop works somehow. If it possible to detect Drop implementation, whats wrong with Copy?

... and if we have specialization ...

We would? I lost my hope for it.

Can't rust check for side effects?

It's hard to accurately determine if a function and every function it calls has externally-observable side effects. Optimization does make use of this information, but the thing about optimization is that it is invisible to the program semantics.

Why may ? There could be false negatives with detecting if type Copy -able?

When I wrote that word I was thinking that it would allow the test to be possibly sound even when specialization isn't, by not guaranteeing the answer to be consistent, but I'm not sure.

Wait a minute - but need_drop works somehow. If it possible to detect Drop implementation, whats wrong with Copy?

Drop has extra rules; for example, this program doesn't compile:

struct Foo<T>(T);
impl<T> Drop for Foo<T> where T: 'static {
    fn drop(&mut self) {}
}
error[E0367]: `Drop` impl requires `T: 'static` but the struct it is implemented for does not
 --> src/lib.rs:3:34
  |
3 | impl<T> Drop for Foo<T> where T: 'static {
  |                                  ^^^^^^^
  |
note: the implementor must specify the same requirement
 --> src/lib.rs:1:1
  |
1 | struct Foo<T>(T);
  | ^^^^^^^^^^^^^^^^^

I'm not an expert on the issues of specialization and I might be wrong, but I think this rule exists specifically so that things like needs_drop can be precise: either Foo<...> implements Drop regardless of its generic parameters, or it never implements Drop regardless of its generic parameters. But Copy does not have any such restriction.

Looking at the function body would make changing the implementation of Clone a breaking change.

Not necessarily, if is_bitwise_copyable is documented as "callers must have the same outward behavior when this returns false" — we don't consider changes in size_of::<Foo>() to be a breaking change by Foo, even though they're observable, unless Foo made promises about its representation.

This is right. You can always assume it's safe to copy instead of clone() if a type impls Copy, regardless of how exactly Clone is implemented. Trying to exclude manual impls of Clone, or even impls containing side effects, would be an anti-feature. There are valid reasons to manually impl Clone: the main one is #[derive] producing incorrect bounds, but there are other possibilities, e.g. you could have a side effect that only takes effect (or is only important) for types that are known to not be Copy. Any manual impls should still be subject to any sort of library-level* clone()-to-copy optimization, just as they are subject to the clone()-to-copy optimizations currently performed by Vec.

So this hypothetical function should simply be "is this type known to be Copy". I am not sure whether it's possible to implement that soundly under the current rustc.

* I do remember some debate about whether or not converting clones to copies is a valid optimization at a compiler level, but that's different.

1 Like

If this is added, it should be needs_clone, like needs_drop.

drop_in_place clone
trivial (trait) T: Copy T: Copy
trivial (const) !needs_drop::<T>() !needs_clone::<T>()
possible T: ?Sized T: Clone

needs_drop is an optimization hint only, and is allowed to spuriously return the safe default true. Similarly, needs_clone could spuriously return true, and is_trivially_copiable = !needs_clone may spuriously return false.

6 Likes

needs_drop exists, because Drop is unusable as a bound. A struct may contain fields with Drop, but not implement Drop itself.

With copying this is not an issue. A copyable field doesn't make its struct Copy. The Copy trait has to be explicitly implemented for the outer type, and can't be inferred from the structure. Otherwise it could undermine guarantees of the newtype pattern.

So I think Copy optimizations are best left to compiler smarts and specialization.

4 Likes

Copy can be implemented for Foo<'static> but not for other Foo<'a>. As such specialization on Copy can't be done in a sound way.

And yet...

So please don't impl Copy for Foo<'static> until specialization gets fixed.

Here's an interesting note, though: the unsoundness of a Copy specialization is that it behaves as a superset of a sound T: Copy, but it's still a subset of "all fields are bitwise copyable" because even a lifetime conditional Copy impl requires all fields are Copy (potentially conditional on the lifetime). While it's still possible to exploit this unsoundness by externally relying on the side effects of Clone happening it seems impossible to exploit internally to T without itself specializing on a lifetime (...potentially hidden behind Self: Copy).

It's also an interesting observation that (in the Unpin case) the root cause of some T not being bitwise cloneäble is necessarily not that its fields aren't — all primitive types are — but that it adds some nontrivial logic on top of those primitive fields.