Summary
Add move($expr) syntax inside closures, async blocks, and generators. A move($expr) evaluates the expression at closure-creation time and captures the result by value. This gives precise control over what a closure captures and when, without needing temporary variables outside the closure.
Motivation
Closures and futures in Rust use an automatic capture mechanism that detects the places that the closure/future body uses from the surrounding environment and decides automatically how to capture them (e.g., by reference, by value). However, it is frequently useful to capture values in other ways; one common example is wishing to capture the clone of a value. Unfortunately Rust's current closure syntax provides no ergonomic way to manage this.
A common pattern for capturing clones, drawn from a blog post by the Dioxus team, is to introduce "dummy variables" into the surrounding scope:
let _some_a = self.some_a.clone;
let _some_b = self.some_b.clone;
let _some_c = self.some_c.clone;
spawn;
The problems with this pattern:
- The temporary variables (
_some_a, etc.) exist only to shuttle data into the closure. They add noise and separate the clone from its point of use. - The
async movecaptures everything by move, which means later code can't useself.some_aeven though the intent was only to move the clones. - When closures are nested or have many captures, the pre-closure variable block grows unwieldy and obscures the actual logic.
Another option is to introduce let variables into a block:
spawn;
This pattern at least avoids the need to introduce "dummy names" like _some_a, but it still has many of the same downsides.
Guide-level explanation
We propose to introduce a move($expr) expressions that can appear in a closure or a future. The expression $expr will execute when the closure is created and then moved into a temporary captured by the closure; move($expr) is then replaced with a place expression referring to this temporary. This can be used to capture a clone of a value (move(vec.clone())) but also other derived values (e.g., move(vec.len())).
move($expr) gives explicit control over captures
Within a closure, async block, or generator, you can write move($expr) to evaluate an expression at closure-creation time and capture the result by value:
||
This moves some_vec into the closure, regardless of how it's used in the body. The expression inside move(...) is evaluated when the closure is created, not when it's called.
Any expression is valid inside move(...):
||
||
||
A common pattern: listing captures at the top
When you want to be fully explicit about what a closure captures, you can list all captures at the top of the body:
||
move || as shorthand
The move keyword on a closure changes the default so that every captured variable is moved in. You can combine this with move($expr) for fine-grained control:
move ||
Here move(&cx.data) creates a reference to cx.data at closure-creation time and captures that reference. The closure borrows cx.data even though it's a move closure.
Async blocks and generators
move($expr) works the same way in async blocks and generators:
spawn;
Closures called multiple times (FnMut, Fn)
When a closure is called multiple times, the captured value persists across calls. A move($expr) captures once at creation time:
data_source_iter
.inspect
.collect;
// `tx` is still usable here
Common patterns with cloning
Cloning a value into a closure is one of the most common uses of move($expr). The right pattern depends on how the closure uses the cloned value and how many times the closure is called.
move(x.clone()) — clone once, consume once (FnOnce)
When the closure is called exactly once and consumes the value, clone directly into the capture:
let handle = spawn;
// tx still usable here
&move(x.clone()) — clone once, borrow on each call (Fn / FnMut)
When the closure is called multiple times but only needs a reference to the cloned value, take a reference to the capture. This avoids consuming the captured clone on the first call:
let f: = Boxnew;
f; // works
f; // works again — data is borrowed, not consumed
move(x.clone()).clone() — clone once at creation, clone again per call (Fn / FnMut)
When the closure is called multiple times and each call needs an owned value, clone the captured value on each invocation:
+ 'static
This is useful when you need a fresh owned copy per call but wish to have the closure own the value (in this example, the closure needs to own the Rc<SharedConfig> so it can satisfy the 'static bound).
Reference-level explanation
Syntax
A move expression has the form:
move ( <expr> )
It is valid only inside the body of a closure, async block, or generator. Using move(...) outside these contexts is a compile error.
Desugaring
A move($expr) desugars into a fresh temporary variable that is:
- Bound to the value of
$expr, evaluated at closure-creation time - Always captured by value (moved into the closure), regardless of how it is used in the closure body
Concretely:
|| foo
desugars to the equivalent of:
where move_capture(tmp) means tmp is captured by move even if the closure body only borrows it.
The temporary is not literally introduced in surface-level Rust. It exists in the compiler's intermediate representation. Its type is inferred from $expr.
Evaluation order
When a closure contains multiple move(...) expressions, the temporaries are evaluated in source order (left-to-right, top-to-bottom) at closure-creation time:
|| f
desugars as:
a() is evaluated before b().
Interaction with move || closures
In a move || closure, all captured variables are moved by default. A move($expr) within such a closure still introduces a temporary that is captured by move, but because the expression can be anything (including a reference) it allows capturing less than full ownership:
move || foo
desugars as:
The closure captures tmp (a reference) by move rather than capturing x by move. The closure borrows x.
Nested closures
Each move($expr) is associated with its immediately enclosing closure. In nested closures, each move(...) creates a temporary for its own level:
||
desugars as:
The outer move(...) captures v.clone() into the outer closure. The inner move(...) then moves that value from the outer closure into the inner closure.
Drop semantics
The temporary introduced by move($expr) is dropped when the closure is dropped. If evaluation of the expression panics, the panic propagates at closure-creation time and the closure is never created. Already-evaluated temporaries from prior move(...) expressions in the same closure are dropped in reverse order, following standard drop semantics.
Type inference
The type of a move($expr) expression within the closure body is the type of $expr. Type inference works as normal. The temporary's type is inferred from the expression, and the use sites within the closure see that type.
Place expression semantics
Syntactically, move($expr) parses as a value expression. However, like other value expressions (e.g., function calls), it can be promoted to a place expression by the compiler when used in a place context (e.g., &move(...) or &mut move(...)). The temporary that is introduced is declared as mutable, so &mut move(x) is always valid without the parent binding of x being declared mut.
||
Closures that are never called
If a closure is created but never called, the captured temporaries are still dropped when the closure is dropped. This is the same behavior as any other captured value.
Parsing
move is already a keyword in Rust. The token sequence move( is unambiguous: since move is not a valid identifier, it cannot be a function call, and since it is not followed by || or |, it is not a closure.
A move expression is an ExpressionWithoutBlock. Its grammar is:
MoveExpression :
move(Expression)
The parser accepts move($expr) in any expression position. The restriction to closure, async block, and generator bodies is enforced as a semantic check, not a grammar restriction.
Because move($expr) uses parentheses and is an ExpressionWithoutBlock, there is no ambiguity with closures: move(x) || y always parses as the binary logical-OR of move(x) and y.
For example, the following is a semantic error because move(x) appears outside a closure:
And move($expr) is parsed as a single expression, so it matches $x:expr in macros:
Feature gate
This feature is gated behind #![feature(move_expressions)]. No edition change is required since move is already a keyword in all editions.
Drawbacks
Another way to spell the same thing
Rust already has move || closures and the let-before-closure pattern. Adding move($expr) is a third mechanism for the same underlying operation. Users must learn when to use which.
Readability of move(...) as an expression
move($expr) looks like a function call but has different evaluation semantics: the expression runs at closure-creation time, not at call time. This is a new concept for readers to internalize. However, the move keyword already exists and already means "take ownership," so the meaning is consistent if not immediately obvious.
Complexity in nested cases
Nested move(move(...)) requires understanding which closure level each move targets. In practice this is uncommon, but the mental model requires understanding the "each move targets its immediately enclosing closure" rule.
Rationale and alternatives
Why not fully automatic reference counting?
This RFC emerged as part of a larger discussion in how to make reference counting more ergonomic. The original goal of that effort was to address the frequest request from higher-level Rust frameworks to have "fully automatic" reference counting, where users do not need to do anything explicit to clone a ref-counted value. This RFC is instead targeting a more modest goal, but one which we believe is also important: making fully explicit clones more ergonomic.
The rationale is this: there are many Rust applications where explicit ref-counting is desirable (e.g., in kernel development or when developing a widely used, perf sensitive library or concurrent data structure). Moreover, even in higher-level applications, there are times when knowing exactly how many references are outstanding is important, such as when working with channels (where dropping the last tx handle causes a rx handle to return None, and hence can be important for correctness) or when working with large ref-counted data structures that consume a lot of memory. Fully automatic reference counting makes some Rust applications much cleaner but does nothing for these scenarios. This RFC, in contrast, helps all Rust users, even if it doesn't go as far as higher-level applications would like. Nothing in the RFC precludes adding fully automatic reference counting as future work.
Why this design over explicit capture clauses?
Explicit capture clauses (e.g., move(a.b.c.clone(), ..) || ...) have been discussed for years. They front-load all capture decisions at the closure head, which helps in some cases but creates problems:
- The capture list grows linearly with captures, duplicating information that's often obvious from the body.
- They introduce a new syntactic position with its own grammar, including place remapping (
a.b.c = expr) and open-ended captures (..). - For short closures, the overhead of the capture list dominates the closure itself.
- They don't integrate naturally with the "clone then capture" pattern when you want the clone at the point of use rather than at the closure head.
Move expressions solve the same problems with less syntax: you write move($expr) at the point of use, which is both the declaration and the consumption.
Why not RFC #3680's .use / use ||?
RFC #3680 proposed a Use trait and use || closures that automatically "used" captures, with the goal of having a "DWIM" closure where it is not required to explicitly clone each value you use. This is a worthy goal but it is not the goal pursued by this RFC. We chose not to pursue this goal first because there are substantial use cases that do not want it. This includes low-level Rust, but also, even in higher-level frameworks, cases like a Sender on a channel or a Bytes buffer, where knowing exactly how many clones exist can be important for avoiding deadlocks or controlling memory usage. Therefore, we are first targeting the version where all clones are explicit. This does not prevent adding a more ergonomic, implicit option later (such as .use or use || closures) to handle the remaining cases more succinctly.
Why not postfix syntax (expr.move)?
A postfix operator would look like expr.move or $expr.move. The problem is that move doesn't operate on the final value of the expression; it changes when the entire expression is evaluated. Consider:
|| process
When does bar() execute? It must be at closure-creation time, but the postfix position suggests it's part of the normal call-time evaluation. A prefix move(...) makes the scope of early evaluation explicit.
What happens if we don't do this?
The clone-before-closure pattern remains the only option. This is verbose but functional. The cost is friction for a common pattern, particularly in async code.
What is the fully desugaring semantics, canonical form for closures?
As a general rule, it is desirable to have a "fully desugared, most general" form of each piece of Rust syntax. For closures, the "fully general form" is to convert a closure into a move closure that only references new temporaries introduced explicitly to be desugared (determining exactly what is captured into those temporaries is non-trivial and require analyzing the body of the closure to see what places it uses and how). The canonical closure form does not change with this RFC, it is simply extended so that move expressions are another way to get one of those temporary values:
// Example 1:
|| foo
// equivalent to:
// Example 2:
move || foo
// equivalent to:
// Example 3:
|| foo
// equivalent to:
Does move($expr) change evaluation order away from source order?
Arguably not more so than closures already do. The body of a closure is already deferred — none of it executes at the point where the closure expression appears in the source. A move($expr) extracts a sub-expression from that deferred body and evaluates it at the point where the closure is created, which is source order for the enclosing scope. This is exactly what the let-before-closure pattern does today:
// Today: `expr` evaluates here, in source order
let tmp = expr;
// With this RFC: same semantics, just written inline
|| use
In both cases, expr executes at the closure-creation site. The move($expr) form simply avoids introducing a named temporary in the enclosing scope.
If I write move(x.clone()) twice, will x be cloned twice?
Yes. Each move($expr) is independently evaluated at closure-creation time:
||
If you want to clone once and use the result multiple times, bind it with let:
||
Note the & — in a non-FnOnce closure, you typically don't want each call to take ownership of the captured clone. Taking a reference lets you use it repeatedly. Alternatively, if you need owned values on each call, you can clone the captured value:
||
Isn't there a kind of "cliff" going from a cloned variable used once to a cloned variable used twice?
Somewhat. Under this design, if you have
||
and then you with to invoke two methods, you likely want to change to
||
which means you wind up writing foobar a total of 4 times. We consider this acceptable if not necessarily optimal. It does have the upside that the execution semantics are quite clear.
Prior art
C++ lambda init-captures (C++14)
C++14 added generalized lambda captures with initializers:
auto closure = ;
This allows arbitrary expressions in the capture list, evaluated at lambda-creation time. The captured variable x is then available in the lambda body.
The design serves the same purpose as move($expr): evaluating an expression at closure-creation time and binding it into the closure. The difference is that C++ places this in a capture list (front-loaded), while Rust's move($expr) is inline at the point of use. C++'s approach requires naming the captured variable explicitly in the capture list, which adds verbosity when the variable is used only once.
Earlier community proposals
This idea has been discussed multiple times in the Rust community:
- Zachary Harrold proposed it on Zulip using the
superkeyword, and created a proc-macro prototype called soupa. - @simulacrum proposed using
moveinstead ofsuper, which better aligns with Rust's existing terminology.
Unresolved questions
Choice of syntax
I'm not convinced that move($expr) is the right syntax. Here are some alternatives keywords I've considered:
capture($expr)(new keyword, though)super($expr)(heavily overloaded keyword, I generally prefermoveas it is already associated with closure capture rules, but open to others' takes)
And then there is the question of () vs {} (thanks @juntyr for raising it earlier!)
move($expr)move { $expr }
Should there be a syntax for moving specific variables?
The compiler's HIR desugaring represents "this variable is captured by move" directly. An explicit syntax like move[x, y] || ... would let users write:
move ||
On one hand, this is a natural complement to move($expr), letting you move specific variables without a full move ||. On the other hand, any closure can already be written using move($expr) at each use site, making this redundant:
||
The tradeoff is conciseness vs. having a single mechanism. This question can be resolved during stabilization.
Future possibilities
Diagnostic suggestions
The compiler could suggest move($expr) when it detects the let-clone-before-move-closure pattern:
help: consider using a move expression
|
- let tx_clone = tx.clone();
- tokio::spawn(async move { send(tx_clone).await });
+ tokio::spawn(async { send(move(tx.clone())).await });
This is left to implementation discretion rather than specified by this RFC.
Integration with a Share trait
A future Share trait identifying types where cloning creates an alias (rather than an independent copy) would compose naturally with move expressions:
spawn;
This is out of scope for this RFC.
move($expr) outside closures
A future extension could allow move($expr) in non-closure contexts as a no-op identity expression, making it possible to write generic code that works both inside and outside closures. Whether this is desirable is left to future discussion.