When should the Copy trait be used?

I'm wondering if there are some general guidelines about when the Copy trait should be used.

I think I generally understand that it should be used on "plain-old data" that resides only on the stack.

I was working with some code that generally looked like:

enum Color {
    Red,
    Blue,
    Green,
}

fn print_color_borrow(color: Color) {
    match color {
        _ => {}
    }
}

Before realizing that I didn't want to move the enum instance, so I figured passing a reference in would solve the problem:

enum Color {
    Red,
    Blue,
    Green,
}

fn print_color_borrow(color: &Color) {
    match *color {
        _ => {}
    }
}

But I was dissatisfied with the additional * making this code feel slightly more complex to read (I probably need to stop thinking in terms of C++). Then I realized that in C/C++ this trival enum would just be implemented as an i32, so why not just copy it?

#[derive(Copy, Clone)]
enum Color {
    Red,
    Blue,
    Green,
}

fn print_color_borrow(color: Color) {
    match color {
        _ => {}
    }
}

I think this is the correct solution, but it made me start thinking about if large enums or structs would incur a performance penalty if copied.

#[derive(Copy, Clone)]
struct BigStruct {
    a: i64,
    b: i64,
    c: i64,
    d: i64,
    e: i64,
    ...
}

Would it be more efficient to pass a reference to BigStruct rather than copy it, even though it is plain-old-data? On say, a modern x86_64 machine is there a cutoff (pointer size?) that a programmer should be thinking about when making these kinds of decisions? Would that change if you were considering other platforms (e.g. 32-bit ARM)?

2 Likes

Personally, I'd impl/derive Copy if semantics allow for it and then pick ref vs copy on a case by case basis. Performance in the presence of an optimizing compiler is a bit hard to reason about generically. For instance, a compiler may inline a function and no copying may be needed. Or it may do a scalar replacement of an aggregate and put the individual fields into registers without physically copying anything. References are probably the sure bet to avoid unnecessary copies though, and Rust's borrowing rules ought to allow compiler to reason well about aliases/borrows.

2 Likes

The other concern here is API evolution. If you export a type that impls Copy, then it's a breaking change to remove that impl in a future release. For example, maybe your Color type grew to support arbitrary names like this:

enum Color {
    Blue,
    Red,
    Other(String),
}

Now maybe this isn't a great example, but my point is that your previous definition of Color can satisfy Copy but this definition cannot.

I wouldn't worry too much about passing around big structs unless you can observe it in a benchmark.

3 Likes

Beyond efficiency and ergonomics concerns, I believe you should think about the semantics of your type. What is it that your type is modelling? What is the responsibility of an instance of your type?

There are types whose main responsibility (whose instances', actually) are modelling domain concepts, purely informative without state or any true functionality. I think this is the case with your enum Color here. An instance of Color represents an entity in your color domain, and as such, one instance of Color::Red talks about the same color as any other instance of Color::Red. A Vec of Colors would fit the same reasoning. Because manipulating two independent copies does not seem to bring any trouble, Copy may be indicated here.

On the other hand, there are types whose role is to hold program state or resources (you may argue that program state is in fact a resource) and to manipulate them. Think of File. An instance of File not only represents an open file in the filesystem, it also holds a cursor that points to the current read/write position. Imagine one copy writes some bytes to the file and then the other one tries to read further. Ever heard about the problems of mutable shared state? You probably don't want two copies of File lying around. Now you have two options: either trust the users of your type to not create copies, or you just plain disable the option: don't implement Copy for objects that hold program state and have the chance to manipulate it.

In summary, you probably want to implement Copy only for value types.

2 Likes