Clone() semantics overloaded, can we fix it?

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.

You can enable this clippy lint.

5 Likes

Clone is just a trait. It has nothing to do with good or bad performance.

3 Likes

You might be interested in Niko's Claim idea, which morphed into a project goal and RFC

4 Likes

(Tangential, but) in line with what @firebits.io said, [u64; 100_000] and the like are Copy too.

1 Like

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.

6 Likes

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 ptr be an Arc, whereas the latter allows any Clone type.

2 Likes

Yes, Claim is what I had in mind (nice short name, BTW), and the pain points are well articulated. I hope it would find its way into Rust. Thank you!

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]


  1. Any method can sneak in some shared state somehow. ↩︎

1 Like