`autoclone` variable marker

Weighing the duplication of code that would require versus the clone would be hard for an optimizer. Vectorization also wouldn't like this one-off case.

I feel like this might be better be doing something like:

for (x, foo) in old_iter.zip(iter::repeat(foo)) {
}

Note that the compiler doesn't know it's the last iteration, in a for loop, generally. A for loop just knows that Iterator::next returned Some(_), so it has no idea whether it's the last iteration.

(LLVM might know, but at the level where it's doing move-checking it definitely doesn't know.)

1 Like

Even for ExactSizeIterator?

TrustedLen + ExactSizeIterator could do specialization of internal iteration to do something different on the last iteration, but for's desugar only uses Iterator::next.

Maybe it'd be possible to have something like

unsafe trait IsLast: Iterator {
    fn is_last(&self) -> bool { false }
}
unsafe default impl IsLast for impl Iterator {}
unsafe impl IsLast for impl TrustedLen + ExactSizeIterator {
    fn is_last(&self) -> bool { self.len() == 1 }
}

and use that in the for desugaring, but now you're complicating the semantics of for a lot just to avoid an automatic clone which was marked as cheap enough to be inserted automatically.

5 Likes

The original problem was the bad ergonomics when cloning things into a closure, because one also needs to introduce a binding holding a variable often named very similar to the original.

The suggestion by @scottmcm of extending move with a list of how variables are captured solves this issue perfectly (at least in my eyes).

Other situations (when needing to clone something a lot) do not have this ergonomic problem of requiring you to create a binding for each clone. You might need to write .clone() a couple of times, but I think the explicit call has multiple benefits:

  • easily scan for uses of clone using text search
  • no implicitly executed code
  • clone is not special, it is just another function (drop on the other hand is special)

While a binding marker like autoclone would improve this situation I think it is unnecessary in the face of better ergonomics for specifying closure capturing.

I am firmly against introducing an AutoClone Trait (because it executes non-special code implicitly), when I come across cloning data, it is one of these situations:

  • I want to turn a reference into something that i own (should be explicit)
  • I need to pass the same data to multiple functions (should be explicit)
  • I need to pass the same data to multiple threads (should be explicit, but without the additional let bindings)

I have not yet seen an example where autoclone/AutoClone would be useful (prevent a lot of clone calls) that is not situation 3 (because there it would also get rid of the additional bindings).

When you really have such extensive usage of clone (situation 1 & 2), why not use a closure to clone your data?

let data = ...;
let data = || data.clone();
consume(data());
consume(data());
consume(data());
consume(data());
consume(data());
consume(data());
consume(data());
consume(data());
consume(data());
consume(data());

Aside from the additional () and creation of the closure, this is identical to the autoclone/AutoClone syntax. But you still have the clone call to find with text search and the explicit closure call indicating that some code is run there.

4 Likes

I am looking at this somewhat differently: Let me start with an example with a str reference.

let s = "some string";
let s2 = s; // This is a copy

With Arc<str> however, if you assign, you move. I think for types like Arc<T> this is somewhat unfortunate.

I discovered in a project of mine that I clone arcs a lot more than I move (about 10 clones to 1 move). This is tedious and distracting.

What if move were explicit and clone implicit?

I decided to try to make a trait AutoClone and a procedural macro to swap the expliciteness: wherever a type is AutoClone to clone it just assign it or pass it as an argument, etc.; and to move it, prefix the expression with move. Then try it out to get a feeling for it.

The difference is: clone stays what it is, just deemphasize it in the code, because it is such a trivial operation compared to move.

Note that that's exactly how C++ works, and to borrow a line from Adams, it has made a lot of people very angry and been widely regarded as a bad move.

We can definitely tweak whether clones can be automatic in more situations than just Copy, but I think requiring extra syntax for moves is a non-starter.

6 Likes

Sorry to disagree respectfully.

First, move in C++ is something else than move in Rust. So if C++ people are angry about their move, so what. :person_shrugging:

Second, there is no extra syntax. Rust already has move closures.

Third, it would be opt-in (don't use Autoclone).

Fourth, I already moved on (accidental pun here happily accepted) to a different (extremely hacky but luckily without any unsafe code) solution.

I discovered today that a procedural macro won't solve this problem. I didn't know that procedural macros work on a syntactic level only. Please forgive me. I never wrote a procedural macro and am here to learn about the deeper aspects of Rust.

Fifth, no, I won't disclose my solution here because it just would make people really angry. Sorry. Peace!

2 Likes

Sorry to disagree respectfully.

Yes, that is true, and the fact that C++ had implicit copies before it had moves contributes to the flames, and even today its moves are not destructive and it does make some people angry, but I still agree with @scottmcm because I do think that implicit copies are part of the story and were mistake. Look at the astonishing amount of std::move()s out there.

I don't understand how is that related?

I am strongly opposed to that: this has the potential to split the ecosystem, which is the last thing I want for Rust (less than implicit clones, even!)

I insist on my opinion that we should provide an easier syntax to cloning and moving to closures that does not include any blocks and/or redundant indentation, and that we should do nothing other than that. Of course, it is perfectly valid to think otherwise :slight_smile:

1 Like

To come out, I have two hats here in this discussion.

First I had to solve a practical problem with cloning. I have immutable string keys for lookups. For that I use arcs. I really have hundreds of cloning calls. Yesterday I found a cheat to reduce (but not remove) the visual clutter. Additionally I can try to make more methods taking references and push the cloning inside them. A clever variant is (thanks @y86-dev):

let data = || data.clone();

Second after this experience of mine I had some deeper, theoretical thoughts. Let me paraphrase first the current situation as I understood it as a programmer.

We have move, copy and clone. Types that are Copy don't need to be build nor dropped so they can just be copied. Others have to decide between move and clone with different trade-offs: move makes the source unusable, clone does not but needs to build a new value which might be expensive. Rust has decided to make clone explicit because of the additional step of building. Types that are Copy have the privilege of being simple, no tradeoffs to be considered here. I find this very well thought out.

As always, however, there are corner cases. I find Arc to be one of them in some circumstances. Of course they are clearly not Copy because they need to increment the reference counter when building and decrement when dropping.

This can get annoying and tedious: when your code treats arcs almost as Copy by almost mindlessly cloning them everywhere, because cloning arcs is relatively cheap. Especially when you also have &static str which is in fact Copy. Suddenly you have to do a distinction between two values which are similar but not really the same. Then that move makes the source unusable starts to bite you. Forget a clone and your code doesn't compile, but for your &static str the clone just adds to the visual clutter. It's a papercut.

Now, I have gone to accept this situation and by using my cheat and pushing clone inside methods, I have grown, to stay with the metaphor, calluses on my hands and don't mind the papercuts so much anymore.

In other words, I got over it and moved on.

Arc clones are not trivial. There is quite a bit of cross-CPU contention in the atomic behind the scenes. You really want to move if at all possible. I can't count the number of times std::shared_ptr<T> as a parameter has been pointed out to be a bottleneck in threaded code because every thread wants to increment the atomic around the same time.

5 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.