`autoclone` variable marker

This pertains to the problem discussed in Clone into closures#2407, which describes a problem of tedium that I frequently run into myself regarding the need to declare intermediate "clone" variables to pass to move closures:

let foo = Arc::new(...);
let foo_clone = foo.clone();
thread::spawn(move || {
    foo_clone.baz();
});
foo.baz();

That discussion had me wondering whether perhaps this could be alleviated by adding a marker keyword for variables analogous to the mut marker, that would instruct to compiler to automatically call clone for us when it needs to, such as when moved into a closure or when passed to a function that takes ownership:

let autoclone foo = Foo::new(...); // Must implement `Clone`, otherwise error
thread::spawn(move || { // Clones `foo`
    foo.baz();
});
takes_ownership(foo); // Clones `foo`
let bar = foo; // End of `foo`'s NLL-scope (it's never used again), no clone required: moves `foo`

The autoclone marker would be allowed in all the same places the mut marker is currently allowed. Like mut, it would apply only to the variable until the end of that variable's scope. It would not be inherited by downstream variables (e.g. bar in the example above is not marked autoclone, the compiler is not allowed to automatically clone bar). It would be allowed along-side the mut marker, e.g.:

let autoclone mut foo = Foo::new(..);

This does introduce more magic. Especially in longer function bodies, one might come across the use of a variable foo that is not obviously autoclone as its declaration happened way earlier in the code. I think this is alleviated by the fact that the effect of autoclone is always limited to a single lexical scope, that is to say, there is only ever one place someone would have to look to determine if a foo variable is autoclone, which is the declaration site for foo. I would also argue that variables that hold Copy values come with a similar (identical?) problem, which could be taken as precedent. One might say that the autoclone marker essentially tells the compiler: treat this variable as if it were Copy (if it isn't copy already), except you must explicitly call the Clone implementation.

Some further considerations:

  • Perhaps disallow the autoclone marker for values that are also Copy, to prevent ambiguity in what happens on a move (e.g. let autoclone foo = Foo::new(); errors if Foo implements Copy).
  • Perhaps an explicit ordering should be defined when autoclone is used alongside mut, e.g. "autoclone must always occur first" (let autoclone mut foo = ...), or "mut must always occur first" (let mut autoclone foo =...).
  • "autoclone" is perhaps a bit long for a keyword, maybe someone can come up with a shorter label that conveys a similar meaning.
4 Likes

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?

Opinion: just always cloning would be more consistent (you know takes(foo) will call clone, no matter what comes after) but runs into the issue that you can't explicitly drop(foo) or otherwise remove the autoclone behavior before scope end in any way. This would encourage only using it for shared-ownership types where the difference between a clone or a move is minimal, but basically preclude optimizations based on discovering ownership is actually unique (e.g. make_mut).

Bad Idea: bring back move $expr as the way to explicitly move out of an autoclone binding. So rather than the optimization being "remove .clone() on the last use," it's "add move on the last use."

I'm actually personally generally positive on this as a solution to the desire for automatic cheap clones. Specifics need to be hashed out, but the general direction directly tackles the pain point.

The one thing to worry about is that (especially in the async programming world) roughly every binding would be labeled autoclone. I think this is mitigable, though; primarily by lints such as

  • Used autoclone for Copy type; remove it (deny by default; I don't think this should be a hard error)
  • Unnecessary autoclone annotation; remove it (warn by default; triggers when only moved from once and not used afterwards)

For the closure case, one thing I know we've talked about before is some kind of explicit capture syntax.

Perhaps something like this:

let foo = Arc::new(...);
thread::spawn(move[clone foo] || {
    foo.baz();
});
foo.baz();

And you could also have other captures there like &bar or &mut qux, with being forbidden to reference anything else in the closure. (So move[] |a| { ... } couldn't capture anything, for example.)

How much would that handle your case? It of course wouldn't solve the auto-clone in non-closure cases.

(Placeholder syntax, of course. Let's not discuss brace choices or similar here.)

10 Likes

Are there cases where the consistency of "always cloning" would make a big difference? Seems a little odd to me to do an extra clone when you know statically that it isn't necessary.

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. Maybe clippy can help with some "unnecessary clone, use &var instead", but this feels, to me, like optimizing for writers, not readers, of code which is not the greatest balance IME.

3 Likes

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 memcpys 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. Maybe clippy can help with some "unnecessary clone , 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.

But Copy is guaranteed to be "trivial". Clone is not. Arc clones introduce cross-cpu communication; Vec can be "large" allocations (sure, one could have a 1KB Copy type, but I think (100% gut-backed) this is far rarer than even a Vec holding onto a 512KB allocation); I'm sure there are more classes of cases, but I'm coming up blank at the moment.

I feel like most cases of "I want this to be cloned everywhere it is used" is centered around closures, not "open" code. In that sense, I think some move[…] C++-like explicit capture model is probably of more interest anyways. The only other place I feel I've wanted it is in builder patterns where I wanted to clone the builder to make some instance A then tweak some other thing to make instance B which is "A with one change". This feels rare enough to me to not warrant further changes at least.

4 Likes

That's the advantage of autoclone (over, say, CheapClone): it's an opt-in at the let pattern binding site. When the binding is introduced, it will say "hey I get implicitly cloned a lot." If you don't want it getting cloned "too much," just don't put the autoclone marker on it.

It's important to note: when a binding is copied, it does not call Clone::clone! A binding being copied is, at the machine level, exactly equivalent to being moved: the bit level representation is copied from the caller's stack into the callee's stack, as prescribed by the ABI[1].

The only difference is what happens after the move. If the type is not Copy, the binding is considered uninitialized[2] and cannot be used anymore. If the type is Copy, it isn't, and it can.

Thus the simple semantics for autoclone is "when this binding would be moved from, call Clone::clone instead." If the last move is to actually move the value out of the binding, more work would have to be done (maybe reusing drop flag machinery?) to determine the point of last use and replace the implicit move-clone (if it is one) with a real move.


  1. The ABI means some tricky things can happen, including pass-by-reference to where it already is, resulting in no copy at all! Though AIUI this optimization is not yet done, for Reasons™, but we'd like to make it possible. ↩︎

  2. Well... technically not yet, moving doesn't actually uninitialize the moved from place, for Reasons™. This is why the optimization in the previous footnote doesn't happen. ↩︎

3 Likes

Could the closure be like this?

[clone foo, move bar] || {
    let _bar = bar;
    foo.baz();
}

But there will introduce ambiguity where arrays can impl the Or operator.

Or use the pattern instead. (Introduces clone as a new pattern mode: capture by cloning, just like ref or something else).

move [clone foo, bar, ref baz, ..] || {
    let _bar = bar;
    foo.baz(&baz);
}

Where move without a pair of brackets desugars to move [..] (capturing all variable by moving automatically).

1 Like

It seems to me that the argument comes down to this: implicit behavior around the passing of ownership is perhaps ok, as long as it is "cheap enough" (quotes to mark vague language). The questions this brings up for me are: What is cheap enough? And who decides?

I think the current answer to the first question is: "cheap enough" is when a bit-wise copy can be made of the value, and such values are identified by whether they implement Copy. As you point out, one might have larger Copy types that are more expensive to copy than others, but in general this is probably a good heuristic for "cheap enough".

The answer to the second question seems to be: the Rust language designers. Or at least, the decision is not really up to the end-user.

Please note that I am not necessarily trying to argue that the current answers to these questions are too conservative (in fact, I'm not sure at all yet where I come down on this myself); I'm just trying to make the argument more explicit.

I think a follow-up consideration is then: are there any cases at all where clone-but-not-copy types can be considered "cheap enough" to allow implicit behavior around the passing of ownership? As I've been thinking more about this idea, I think I've come to believe that if autoclone is a good idea at all, then it needs to be accompanied by an opt-in AutoClone marker trait. That essentially creates a "double opt-in" situation, where both the type author believes it is "cheap enough" in the general case and the end-user thinks it is "cheap enough" in their specific lexical scope.

It's perhaps worth pointing out that the particular niche in which I've been running into this a lot is compile-to-WASM web programming, specifically with wasm-bindgen's JsValue type and wrapper types of JsValue (essentially all of js-sys and web-sys). Let's say for simplicity's sake that for all intends and purposes, such types behave like Rc. When doing web programming, such JsValue wrapper types are verry pervasive. Could Rc be considered "cheap enough" and implement AutoClone? Could JsValue and it's wrapper types be considered "cheap enough" and implement AutoClone? I ask this mostly, because if the Rust community would generally answer "No" for even these types, then there is probably no viable use-case for autoclone.

I'm not particularly knowledgable about the lower level assembly results of compiled Rust code, so I this may well be based on poor knowledge, but to my understanding, what "passing ownership of a Copy type" compiles down to is situationally dependent. For example, I recall reading that for passing ownership of Copy types above a certain size, the optimizer may decide not to do copy the entire value into the caller's stack frame at all, it may instead opt to essentially pass the value by reference.

I guess this is what had me wondering about the "copy semantics" at a more abstract, higher level, rather than at at the practical level after optimization. It seems to me that they are something along these lines (again please excuse the vague language): if a variable holds a Copy type, then you may pass ownership of that variable more than once; the compiler must make this work for you, and is allowed to do whatever it thinks best to achieve this. That is to say: in my (very basic) understanding, the actual bare metal translation of "copy semantics" seems explicitly left undefined.

I guess I'm fond of the idea of "autoclone semantics" being analogous to "copy semantics" (at the abstract, higher level), except you give the compiler an additional tool: if you mark a variable autoclone, then the compiler is now also allowed to call Clone::clone to "make it work", if it has to.

1 Like

Quick follow-up that I wish I had included in the previous post: I've also been wondering about problem of side effects in Clone implementations. I think this issue would arguably be resolved by an opt-in AutoClone trait, if its comes with some accompanying implementation prescriptions in its documentation. One would not be able to depend on any side effect in Clone implemenations. Types that would rely on such side effects should not implement AutoClone. The compiler would never be forced to implicitly autoclone just for the side effects, it only autoclones for the purpose of obtaining a new "owned value" when it needs to. The compiler would be allowed to e.g. optimize away entire code-paths, without having to call Clone::clone just for the side-effects due to implicit autoclones.

I would say it absolutely is, actually.

"It doesn't allocate" might be a reasonable definition for CheapClone.

That would be reasonable, I think. Apparently we can't have Clone do that (https://github.com/rust-lang/rfcs/pull/3197) but a new trait we could easily enough.

I'm not convinced the double-opt-in would be necessary. It seems to me like it would result in arguments in PRs when people want to use autoclone in their code but the library author doesn't like autoclone.

I think if you really want a let autoclone v: Vec<String>, then fine. With the opt-in on the binding, if people want to opt-in, then I don't think they need to be blocked from doing it. (We can always have clippy lints for things like "hey, that's a generic type; maybe you shouldn't autoclone it".)

3 Likes

So... why not go the other way and use type-side AutoClone only?

  1. Add std::clone::AutoClone trait, AutoClone: Clone
  2. Implementing AutoClone implies that Clone::clone() does not have side-effects (and may be optimized out)
  3. use std::clone::AutoClone in a module opts-in to implicit cloning; without this clone must be explicit even for types implementing AutoClone

Required: guidance on when AutoClone should be implemented (side-effect free, "cheap" by some measure — e.g. no allocations?).

Clarification: what is a side-effect? Presumably Rc::clone is considered side-effect free, yet a clone will prevent Drop of the inner value which might have been expected?

Minor problem: existing libraries must be updated to enable AutoClone.

Minor problem: it only solves the original problem of automatic cloning of captures into closures for a sub-set of variables. But arguably you don't want automatic cloning where this has side-effects or expensive copies anyway.

Oh please don't. I don't want C++-style move/copy-ctor-elision with Rust.

This problem is subset of a bigger problem: your AutoClone is not really an automatic clone, despite the name, it is more like C++'s copy constructor and move constructor combined. That is, it is orthogonal to Rust moves, too. And since Rust moves are destructive we don't have to worry about drop, but your proposal is introducing non-destructive move C++-style into Rust and then the "move ctor" (i.e. AutoClone) also must have a way to signal the the destructor it should not be executed.

I think it is just not worth the effort: C++ really needed move constructors because copies are everywhere, but if only a handful of cheaply-copyable types will implement AutoClone we can just not have copy elimination.

That being said, I personally don't want AutoClone and prefer explicit cloning for closures, something like move [a, b, c] || {} or similar.

5 Likes

I also think just adding or removing a use for a trait that doesn't get explicitly used (even via a named method use) in the module is also a dangerous precedent. Adding a conflicting trait to a module can cause one to have to do <f as Trait>::method() stuff, but doing that makes the code more explicit while this is making the code more implicit.

I don't understand how this is related, sorry.

Sorry, was replying to this bullet point:

1 Like

Poor wording on my part perhaps, but that was not my intention with this design. See the previous two posts: this quote and the reply by scottmcm:


Overall I'm not convinced that this type of addition is a net benefit. I'm just annoyed that the moment a type has a reference counter or any other non-bitwise-copy component, it must be explicitly copied. This, despite that a Drop destructor (when implemented) is always called implicitly.

So you're just proposing treating is as an always-pure function? Fair. However I'm not really sure this is necessary: I think LLVM can infer the purity itself.

EDIT: this was not intended to be a response to a specific message.

In addition to closure capture, I'm sure I had code that looked like this:

let foo = Foo::new(); // Clone but not Copy
for … {
    bar(foo.clone()); // clone is useless in the last iteration
    …
}

Some let autoclone foo = … marker would have made it obvious to the compiler that the clone in the last iteration could be omited and replaced by a move.