To clarify: would moving from an
autoclone
binding a) always clone, or b) clone, except for the last time it is moved, which would just do a move?
I would perhaps describe these options as:
a) When a variable is marked autoclone
, the compiler is allowed to call Clone
on move (as necessary to make your code compile)
b) When a variable is marked autoclone
, the compiler must call Clone
on move (regardless of whether it is necessary to make your code compile)
This had me wondering which of these options apply to "copy semantics" in the "abstract machine" sense (because when it comes to actual memcpy
s in a practical machine sense, after the optimizer is done with your code, I expect the answer is "it depends"). I can't seem to find this explicitly spelled out anywhere, perhaps someone knows if this is defined somewhere, or if this is undefined. My expectation is that it uses option a), which is also what I intuitively had in mind for autoclone
variables, though I certainly understand the appeal of the consistency of option b). I think with option a), explicitly dropping would also not be as much of an issue (although if you were to continue to use the variable after the explicit drop, the explicit call to drop would have called Clone
and then immediately dropped said clone).
How much would that handle your case? It of course wouldn't solve the auto-clone in non-closure cases.
Speaking for myself, the main places I run into this are with move
closures and with async move
blocks. I would expect that any solution more specific to move
closures would also apply to async move
blocks, so such a solution would probably address the main pain-point. I would not mind such a solution, although such solutions as proposed in 2407 seem to only bring a minor improvement in terms of verbosity.
I suppose this autoclone
idea came to mind reading this proposal, which proposes a "cheap to clone" marker trait called Capture
. Instead of a move
closure, one would then define a capture
closure:
capture || {
...
}
This closure is then allowed to implicitly clone any variable in scope that implements Capture
.
That solution seems to place the decision of what is ok to implicitly clone with upstream library authors, the author of a data structure decides whether it can be implicitly cloned. This autoclone
solution places the decision with the user: the user decides if this specific variable is ok to clone in this specific scope. Though of course we're talking about implicit compiler behavior, this autoclone
idea seems to give more explicit control than the capture
idea.
Note that you could require agreement between the author of the data structure and the client user, with an AutoClone
marker trait:
pub trait AutoClone: Clone {}
Then, only types that implement AutoClone
could be marked autoclone
, or else the compiler errors. I suppose such an AutoClone
trait could be opt in (requiring the data structure author to implement it explicitly) or opt-out (where it is automatically implemented for any type that implements Clone
unless the author specificly provides a "negative impl" for AutoClone
).
FWIW, I think I'd rather see where clones are happening rather than getting to some point, seeing a "final "clone", move instead" marker and then having to go back and make sure that the thing isn't cloned "too much" for any perf reasons. Sure, API designs should prefer slices to
Vec
if read-only access is needed, but…that doesn't always happen. Maybeclippy
can help with some "unnecessaryclone
, use&var
instead", but this feels, to me, like optimizing for writers, not readers, of code which is not the greatest balance IME.
I would point out that arguably the same problem exists for Copy
types. In fact, I might argue that the problem is worse for Copy
types, because no copy
marker is required on the variable. While this autoclone
idea is not explicit at the expression level, it is explicit at the lexical scope level. The same is not true for Copy
, one would have to look up the actual type declaration to verify that the type is Copy
, which typically resides in another codebase entirely. I suppose one typically assumes that a type is Copy
because you see it being used after move, and with most code we're looking at, the implicit assumption is that we are in fact looking at code that will successfully compile.