Motivation
Consider we have functions foo
, bar
, qux
, which all have signature fn(Arc<X>) -> ()
:
let x = Arc::new(X);
foo(x.clone());
bar(x);
Now we have a new commit that additionally calls qux
later on, so we have this diff:
- bar(x);
+ bar(x.clone());
+ qux(x);
This corrupts the git diff. And bar(x.clone())
is sometimes far away from qux(x)
and mixed with other parameters, making the diff confusing.
This argument is somewhat similar to that of permitting trailing commas behind the last field of a struct.
Why don't we just clone all the time?
If we instead just clone
every time, it would involve an unnecessary clone, which would be costly for some times (such as Box<X>
).
It is not possible for the compiler to optimize away the clone in most non-derived Clone
implementations. For example, consider the simple case of Rc
, which is not even costly:
fn main() {
let y = Rc::new(Y);
x(Rc::clone(&y));
}
fn x(y: Rc<Y>) {
println!("{}", Rc::strong_count(&y));
}
The compiler cannot optimize away the Rc::clone
, because the original Rc is not dropped until the end of the program.
(In fact, this is an actual problem because calling drop(y)
in x
does not free the memory allocated for Y
)
Example
It would be cool if we could write the above in the following syntax:
let x = Arc::new(X);
foo(x~.clone());
bar(x~.clone());
qux(x~.clone());
which is equivalent to the following:
let x = Arc::new(X);
foo(x.clone());
bar(x.clone());
qux(x);
Reference-level explanation
(Compare to method call expression a.b()
)
Syntax:
MoveableMethodCallExpression:
Expression
~.
Ident(
CallParams ?)
Suppose expression
resolves to a value of type T
, and a method T::method(&self, call_params: CallParams) -> T
is available in scope.
Now compiler sees the expression expression~.method(call_params)
.
If expression
can be immediately moved (i.e. it is the last occurrence of the expression, and it is not used in a loop subblock after expression
is last written into), expression~.method(call_params)
is compiled into {call_params; expression}
(resolved the parameters call_params
(?), but does not call .method
).
Otherwise, expression~.method(call_params)
is simply compiled into expression.method(call_params)
.
Note that a moveable method call on the same expression (or any intermediates in its path) always uses (be it borrow or move) the expression, so multiple calls to the same variable would only move the last one.
Concerns
Feasibility in compiler
I am not familiar with compiler internals, so I am not sure if it is easy for a compiler to tell whether a value can be moved. It seems intuitive to a human whether a value is "used" again later (simply check the word doesn't appear again later except in an else
branch), but I am not sure if there are edge cases not covered.
Limitations
This only works for method call expressions. What if we want to use a similar trick in other expressions?
(Non-method) Call expressions
// how do we annotate that this can be optimized into `x` if `x` is moveable?
Arc::clone(&x)
In particular, what if the function takes multiple parameters? (See "Unresolved questions")
Others
I cannot think of any other use cases. Most operators are not applicable here since they take parameters by value already. Index
can probably fit in here, but I don't know any practical case where x[&y]
can be elided into y
.
Unresolved questions
Resolving call parameters
Suppose we call x~.y(z())
. If x
is moveable, do we still need to call z()
?
If we don't need to resolve the parameters, this greatly increases compiler complexity,
because x~.y(x~.y(x2))
would be tricky to decide on.
For simplicity, if we want to get a feature like this quickly implemented, we could just require that x~.y()
must not take any parameters since the only practical use case I can think of so far is Clone
.