I'm writing software with a complex configuration that needs to be visible to multiple threads. I considered different ways to share it, and eventually settled on using Arc to allow cheap cloning of the data (I also considered scoped threads and LazyLock). Several months later, I was reorganizing the code and removed Arc. The code continued to work. Wonderful, I though, something has changed, and I don't need it anymore. Then I realized that I replaced a cheap clone() with an expensive clone(). In my case, the performance degradation was trivial, but it was still unsettling that Rust allowed me to degrade the code without any warnings, not even from clippy with most stringent setting.
I feel that Rust is not true to its ethos by using the same name for expensive clone operations and for functionality designed to be cheap.
It's not just the issue of naming. I can require the Copy trait on a type to ensure it's quick to copy. But I cannot impose a limitation in the code that only cheap clone is allowed, and the expensive clone is not.
I think maybe there should be another trait for the types that promise the cheap clone functionality. Not sure about the name. Maybe trait FastClone with fast_clone(). I feel some out-of-the-box thinking would help.
If you want to emphasize "cheap" clones, you can write Arc::clone(&x) instead of x.clone(), and it'll stop compiling if you make it not an Arc. Whether that's worth doing is up to you.
The problem with FastClone is that it doesn't compose. If T: FastClone and U: FastClone, does that mean (T, U): FastClone? Both "yes" and "no" are bad in various ways.
While performance may be the symptom you noticed, this kind of change has a semantic risk I consider more troubling - if anything, I think the performance degradation you saw is useful because it brought your attention to an unintended and possibly important functional change.
Calling clone() on an Arc<T> creates a second pointer to the same value. Both the original and the clone continue to refer to the same value as long as they exist; changes made "through" one will always be visible through the other. However, this is a behaviour idiosyncratic to shared pointer types and a few other contexts; generally, code that calls clone() on arbitrary types expects the returned clone to be a new and independent value.
Neither use is "more correct" than the other, but they have significant semantic differences. It's probably unfixable now (making Rc, Arc, and so on non-Clone would break a lot of code today, most of which does work fine), but it's something I wish had been designed differently from the start. A separate trait representing that you can create addtional owned handles to an object would have allowed code to differentiate between these two kinds of cloning more easily.
If you want to avoid this when cloning values that have shared-copy semantics, you can always use fully-qualified method call notation to make sure your code breaks if you refactor out the shared-copy type:
Arc::clone(&ptr)
and
ptr.clone()
do the same thing, but the former requires that ptrbe an Arc, whereas the latter allows any Clone type.
Does it? Do you mean, some specific type you're just not completely familiar with yet? It's a reasonable default assumption I suppose.
Or do you mean generics? If so, well... maybe if the author doesn't fully understand Clone yet. We definitely need the current semantics of cloning a Vec<Rc<_>> and HashSet<&_> to exist...
A best effort/advisory trait, akin to Eq, is still possible. If you wanted guarantees, it'd have to be an unsafe trait, as there aren't really any tenable ways to automatically enforce "deep cloning".[1]
Any method can sneak in some shared state somehow. ↩︎