Why is Copy a subtrait of Clone? Understanding the design rationale behind semantic inconsistency

Hi everyone,

I've been thinking about the Copy/Clone trait relationship and struggling to understand the design rationale. I've searched extensively but couldn't find discussions about this specific design decision, so I'm hoping someone here can help clarify or point me to relevant resources.

Current Situation

Currently, Copy is a subtrait of Clone, which allows this potentially confusing behavior:

#[derive(Copy, Debug)]
struct Example(u32);

impl Clone for Example {
    fn clone(&self) -> Example {
        Example(1)  // Different behavior than assignment!
    }
}

fn main() {
    let a = Example(2);
    let b = a;         // Copy semantics: Example(2)
    let c = a.clone(); // Custom Clone: Example(1)
    println!("{:?} {:?} {:?}", a, b, c); // Example(2) Example(2) Example(1)
}

The Semantic Issue

According to RFC 1521, Copy types should guarantee that Clone::clone == ptr::read for T: Copy. However, the current design allows custom Clone implementations that can violate this invariant, leading to potentially confusing behavior where assignment and .clone() do different things.

Alternative Design I'm Considering

I keep wondering why Rust didn't adopt this alternative approach:

  1. Make Copy and Clone orthogonal (no inheritance relationship)
  2. Provide a implementation: impl<T: Copy> Clone for T in libcore for all types that implements Copy to have a ptr::read based clone() implemention.
  3. This would guarantee semantic consistency: Copy types always clone via ptr::read
  4. Generic code could use T: Copy for "cheap implicit copy that always ptr::read" vs T: Clone for "potentially expensive explicit copy"

Benefits of Alternative Approach

  • Semantic consistency: Eliminates the assignment vs .clone() behavior mismatch
  • Type system simplification: Removes the subtrait relationship
  • Better expressiveness: Generic bounds more clearly express intent
  • Improved UX: No need for #[derive(Copy, Clone)] - just #[derive(Copy)]

Migration Feasibility

This seems technically feasible:

  • Edition mechanism could handle the transition
  • Automatic fix tools could remove conflicting Clone implementations
  • The blanket impl pattern is common in std library

My Questions

  1. What am I missing? The alternative design seems to offer better semantics, simpler implementation, and improved developer experience. Are there technical constraints or design considerations I'm overlooking?

  2. Where can I find discussions about such design decisions? I'd love to read the original discussions that led to the current Copy/Clone relationship, but I'm not sure where to look for historical design rationale.

I'm genuinely curious to understand the reasoning behind this design choice. Any insights or pointers to relevant discussions would be greatly appreciated!

Thanks for your patience with my questions!

Copy is a subtrait of Clone, not a supertrait.

However, the current design allows custom Clone implementations that can violate this invariant, leading to potentially confusing behavior

That is true for all traits. If an implementer doesn't obey the API then surprising things happen. Garbage in, garbage out. I don't see what makes Clone/Copy special here.

  • Provide a implementation: impl<T: Copy> Clone for T in libcore for all types that implements Copy to have a ptr::read based clone() implemention.

I guess that didn't happen because RFC 1521 happened after Rust 1.0.

1 Like

Sorry, I'm always confusion with super and sub trait :frowning:

That is true for all traits. If an implementer doesn't obey the API then surprising things happen. Garbage in, garbage out. I don't see what makes Clone/Copy special here.

But that's the philosophy for C++, not for Rust. Rust should make interface that do wrong things hardly. Even there is a lint that warnings for your as Clone has different behavior with Copy.

I guess that didn't happen because RFC 1521 happened after Rust 1.0.

But as I mentioned, Edition should make use for this incompatible.

This would make it impossible to have generic implementations for generic containers. For example, this program does not compile:

trait MyCopy {}
trait MyClone {}

impl<T: MyCopy> MyClone for T {}

impl<T: MyClone> MyClone for Option<T> {}
impl<T: MyCopy> MyCopy for Option<T> {}
error[E0119]: conflicting implementations of trait `MyClone` for type `Option<_>`
 --> src/lib.rs:6:1
  |
4 | impl<T: MyCopy> MyClone for T {}
  | ----------------------------- first implementation here
5 |
6 | impl<T: MyClone> MyClone for Option<T> {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Option<_>`

One could imagine language features that make these implementations not conflict, but we don't have one available yet.

3 Likes

As Copy and Clone defined in the standard library, which can use unstable features even in stable Rust, the impl may do in this way:

#![feature(min_specialization)]

default impl<T: Copy> Clone for T {...}

That should make the container can implement it's own Copy or Clone impl.

That might be possible, but std follows a rule that specialization is not used in a way that can be observed in the API (until such time as we have a sound design and implementation of specialization). So, since that impl would not be possible without specialization, it may not be provided.

2 Likes

So, is it that means, If someday we have min_specialization stabilized, then there could be a chance that change Copy & Clone into orthogonal in future?

Weren't Copy and Clone independent at the beginning?

My take for item 1 is that Copy is a subtrait of Clone because it changes the semantics from move to copy, which restricts its use to specific types that hold no resources, are ideally "fast" and "cheap" to copy, and so on, so in a word, that fit a memcpy behaviour. So copy types are a subclass of clone types, and it's interesting to keep that relationship because making them orthogonal would introduce a confusion: must the compiler use move or copy semantics? That also discards item 2.

(On a side note, I don't really like to say it's an inheritance relationship: it's a bound—a condition. A subtrait doesn't automatically implement the supertrait; you have to do it manually so that a given type meets the condition of the subtrait.)

I don't have any answer for the ability of the programmer to design a faulty Clone implementation, except what's in the Clone trait documentation:

Types that are Copy should have a trivial implementation of Clone . More formally: if T: Copy , x: T , and y: &T , then let x = y.clone(); is equivalent to let x = *y; . Manual implementations should be careful to uphold this invariant; however, unsafe code must not rely on it to ensure memory safety.

At some point, you have to be able to rely on the programmer not to do completely foolish things.

1 Like

Editions aren't magic, they primarily change the surface language in a local manner. And name resolution a bit.
Traits involve global reasoning, to my knowledge we can't have trait impls that magically disappear in some scopes but not in others.

2 Likes

No, min_specialization does not allow writing that impl unless Copy becomes a specialization-safe trait, but that would be a breaking change.

1 Like

IMO, this is a good example of a solution in search of a problem. You've demonstrated that a semantic inconsistency is possible, but you have not demonstrated that it is a problem in practice.

A similar semantic inconsistency is possible with manual implementations of PartialEq, PartialOrd and Ord.

As far as I know, inconsistencies in these trait implementations are not problems plaguing Rust programmers. So trying to solve this problem doesn't seem well motivated.

As for the history, I don't believe specialization was a thing at Rust 1.0. My recollection is that it landed a bit later. So any design that would require specialization to work wouldn't have been plausible.

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.