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:
Make Copy and Clone orthogonal (no inheritance relationship)
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.
This would guarantee semantic consistency: Copy types always clone via ptr::read
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
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?
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!
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.
Sorry, I'm always confusion with super and sub trait
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.
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.
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.
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.
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.