Why does Rust not allow the copy and drop traits on one type?
Asked Answered
R

3

23

From the book:

Rust won’t let us annotate a type with the Copy trait if the type, or any of its parts, has implemented the Drop trait. If the type needs something special to happen when the value goes out of scope and we add the Copy annotation to that type, we’ll get a compile time error.

Why was the design decision made to disallow Copy and Drop on the same type?

Rossuck answered 6/8, 2018 at 9:5 Comment(1)
This is kinda-sorta a formalization of what C++ calls the "Rule of Three".Ignition
N
27
  • The Drop trait is used in an RAII context, typically when some resource needs to be released/closed when the object is destroyed.
  • In the other hand, a Copy type is a trivial type that can be copied with a memcpy only.

With those two descriptions, it is clearer that they are exclusive: it makes no sense to memcpy nontrivial data: what if we copy the data, and we drop one of the copies? The inner resource of the other copy will not be reliable anymore.

In fact, Copy in not even a "real" trait, in that it does not define any function. It is a special marker that says to the compiler: "you can duplicate myself with a simple bytes copy". So you cannot provide a custom implementation of Copy, because there is no implementation at all. However, you can mark a type as copyable:

impl Copy for Foo {}

or better, with a derive:

#[derive(Clone, Copy)]
struct Foo { /* ... */ }

This builds only if all the fields implement Copy. Otherwise, the compiler refuses to compile because this is unsafe.


For the sake of an example, let's suppose that the File struct implements Copy. Of course, this is not the case, and this example is wrong and cannot compile:

fn drop_copy_type<T>(T x)
where
    T: Copy + Drop,
{
    // The inner file descriptor is closed there:
    std::mem::drop(x);
}

fn main() {
    let mut file = File::open("foo.txt").unwrap();
    drop_copy_type(file);
    let mut contents = String::new();

    // Oops, this is unsafe!
    // We try to read an already closed file descriptor:
    file.read_to_string(&mut contents).unwrap();
}
Norean answered 6/8, 2018 at 9:14 Comment(8)
"With those two descriptions, it is clearer that they are exclusive" what about an implementation of a shared pointer?Rossuck
@Rossuck When you copy a shared pointer, you have to increment the inner reference counter. That's not a simple memcpy. See the drop implementation of Rc for example.Norean
So Rust does not allow custom implementations of Copy?Rossuck
@Rossuck Nope. Let me add more information about this in my answer.Norean
@Rossuck actually rust allows something like a custom implementation of Copy. It is called Clone. Clone is always explizit, can perform additional operations (such as increasing a reference count), and can coexist with Drop.Fiddle
Is Copy trait about byte copying from the stack only and not from, for example, the heap? In other words, is this trait a tool for saying the compilier "you can copy bytes from stack for another variable, that's sufficient to do" and if some type presupposes a heap resource, then Copy has to avoided (cause the compilier don't trail heap resources)?Lemmons
Every Copy is also Clone, and they both don't care about the data emplacement. If you want to know if the copies are optimized away, I think that this answer is still relevant: #38571770 @MergasovNorean
@Mergasov the Copy trait doesn't know anything about stack vs heap. For example, if I have some &i32, that might point to something on either one. (Or neither. It could also point to a static value in the "data segment", or anything else that's addressable in memory.) Regardless, the fact that i32 is Copy means I can dereference that pointer and copy the value behind it.Hautegaronne
E
15

The other answers here are talking about why we don't usually want to implement both Copy and Drop for the same type, but that's not the same as explaining why it's forbidden. It might seem like a toy example like this should work just fine:

#[derive(Copy, Clone)]
struct Foo {
    i: i32,
}

impl Drop for Foo {
    fn drop(&mut self) {
        // No problematic memory management here. Just print.
        println!("{}", self.i);
    }
}

fn main() {
    let foo1 = Foo { i: 42 };
    let foo2 = foo1;
    // Shouldn't this just print 42 twice?
}

But indeed, if we try to compile that (using Rust 1.52), it fails as expected:

error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor
 --> src/main.rs:1:10
  |
1 | #[derive(Copy, Clone)]
  |          ^^^^ Copy not allowed on types with destructors
  |
  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0184`.

See the "For more information" note at the bottom? Those are often helpful. Let's run rustc --explain E0184:

The `Copy` trait was implemented on a type with a `Drop` implementation.

Erroneous code example:

```
#[derive(Copy)]
struct Foo; // error!

impl Drop for Foo {
    fn drop(&mut self) {
    }
}
```

Explicitly implementing both `Drop` and `Copy` trait on a type is currently
disallowed. This feature can make some sense in theory, but the current
implementation is incorrect and can lead to memory unsafety (see
[issue #20126][iss20126]), so it has been disabled for now.

[iss20126]: https://github.com/rust-lang/rust/issues/20126

Following that issue link leads to a discussion of "zeroing-on-drop". Present-day Rust doesn't do this anymore, but up until around 2016 Rust implemented "dynamic drop" by zeroing all the bits of a value when dropping it. But of course that isn't a valid implementation if a type can be both Copy and Drop -- Rust can't zero out a value that you're allowed to keep using -- so implementing both of those traits on the same type was disallowed. The discussion ends with this interesting comment:

Anyhow, it's easiest to forbid it for now. We can always make it legal later if someone comes up with a persuasive use case. Idempotent destructors seem like a bit of an odd thing.


What's above is the explanation for Rust's current behavior, as best I can tell. But I think there's another reason to keep things the way they are, which I haven't seen discussed: Copy currently implies that a value can be both bitwise copied and also bitwise overwritten. Consider this code:

#[derive(Copy, Clone)]
struct Foo {
    i: i32,
}

fn main() {
    let mut ten_foos = [Foo { i: 42 }; 10];
    let ten_more_foos = [Foo { i: 99 }; 10];
    // Overwrite all the bytes of the first array with those of the second.
    unsafe {
        std::ptr::copy_nonoverlapping(&ten_more_foos, &mut ten_foos, 1);
    }
}

This unsafe code is totally fine today. In fact, [T]::copy_from_slice will do exactly the same thing for any T: Copy. But would it still be ok, if Foo (or any other Copy type) were allowed to be Drop? Our code here, and the standard library code in copy_from_slice, would be destroying objects without dropping them!

Now, technically, failing to call the destructor of an object is allowed. There was a very interesting discussion back in the day that led to std::mem::forget going from unsafe to safe shortly before Rust 1.0. So it's possible that Rust could allow Copy + Drop without leading to any undefined behavior, despite this issue. But it would be quite surprising that certain (standard!) functions would fail to call the destructors you expect. The property that "Copy objects can be bitwise copied and bitwise overwritten" seems like a good one to keep.

Expiry answered 22/5, 2021 at 3:48 Comment(0)
S
8

Quoting the documentation.

[...] [A]ny type implementing Drop can't be Copy, because it's managing some resource besides its own size_of::<T> bytes.

Sc answered 6/8, 2018 at 9:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.