Closure types
A closure expression produces a closure value with a unique, anonymous type that cannot be written out. A closure type is approximately equivalent to a struct which contains the captured values. For instance, the following closure:
generates a closure type roughly like the following:
// Note: This is not exactly how it is translated, this is only for
// illustration.
struct Closure<'a> {
left_top : &'a mut Point,
right_bottom_x : &'a mut i32,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.left_top.x += 1;
*self.right_bottom_x += 1;
format!("{:?}", self.left_top)
}
}
so that the call to f
works as if it were:
// Note: This is not valid Rust due to the duplicate mutable borrows.
// This is only provided as an illustration.
f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.left_top.x });
Capture modes
A capture mode determines how a place expression from the environment is borrowed or moved into the closure. The capture modes are:
- Immutable borrow (
ImmBorrow
) — The place expression is captured as a shared reference. - Unique immutable borrow (
UniqueImmBorrow
) — This is similar to an immutable borrow, but must be unique as described below. - Mutable borrow (
MutBorrow
) — The place expression is captured as a mutable reference. - Move (
ByValue
) — The place expression is captured by moving the value into the closure.
Place expressions from the environment are captured from the first mode that is compatible with how the captured value is used inside the closure body. The mode is not affected by the code surrounding the closure, such as the lifetimes of involved variables or fields, or of the closure itself.
Copy
values
Values that implement Copy
that are moved into the closure are captured with the ImmBorrow
mode.
Async input capture
Async closures always capture all input arguments, regardless of whether or not they are used within the body.
Capture Precision
A capture path is a sequence starting with a variable from the environment followed by zero or more place projections that were applied to that variable.
A place projection is a field access, tuple index, dereference (and automatic dereferences), or array or slice index expression applied to a variable.
The closure borrows or moves the capture path, which may be truncated based on the rules described below.
For example:
Here the capture path is the local variable s
, followed by a field access .f1
, and then a tuple index .1
.
This closure captures an immutable borrow of s.f1.1
.
Shared prefix
In the case where a capture path and one of the ancestor’s of that path are both captured by a closure, the ancestor path is captured with the highest capture mode among the two captures, CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode)
, using the strict weak ordering:
ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
Note that this might need to be applied recursively.
Overall this closure will capture u
by ByValue
.
Rightmost shared reference truncation
The capture path is truncated at the rightmost dereference in the capture path if the dereference is applied to a shared reference.
This truncation is allowed because fields that are read through a shared reference will always be read via a shared reference or a copy. This helps reduce the size of the capture when the extra precision does not yield any benefit from a borrow checking perspective.
The reason it is the rightmost dereference is to help avoid a shorter lifetime than is necessary. Consider the following example:
If this were to capture m
, then the closure would no longer outlive 'static
, since m
is constrained to 'a
. Instead, it captures (*(*m).a)
by ImmBorrow
.
Wildcard pattern bindings
Closures only capture data that needs to be read.
Binding a value with a wildcard pattern does not count as a read, and thus won’t be captured.
For example, the following closures will not capture x
:
This also includes destructuring of tuples, structs, and enums. Fields matched with the RestPattern or StructPatternEtCetera are also not considered as read, and thus those fields will not be captured. The following illustrates some of these:
Partial captures of arrays and slices are not supported; the entire slice or array is always captured even if used with wildcard pattern matching, indexing, or sub-slicing. For example:
Values that are matched with wildcards must still be initialized.
Capturing references in move contexts
Because it is not allowed to move fields out of a reference, move
closures will only capture the prefix of a capture path that runs up to, but not including, the first dereference of a reference.
The reference itself will be moved into the closure.
Raw pointer dereference
Because it is unsafe
to dereference a raw pointer, closures will only capture the prefix of a capture path that runs up to, but not including, the first dereference of a raw pointer.
Union fields
Because it is unsafe
to access a union field, closures will only capture the prefix of a capture path that runs up to the union itself.
Reference into unaligned struct
s
Because it is undefined behavior to create references to unaligned fields in a structure,
closures will only capture the prefix of the capture path that runs up to, but not including, the first field access into a structure that uses the packed
representation.
This includes all fields, even those that are aligned, to protect against compatibility concerns should any of the fields in the structure change in the future.
Similarly, taking the address of an unaligned field also captures the entire struct:
but the above works if it is not packed since it captures the field precisely:
Box
vs other Deref
implementations
The implementation of the Deref
trait for Box
is treated differently from other Deref
implementations, as it is considered a special entity.
For example, let us look at examples involving Rc
and Box
. The *rc
is desugared to a call to the trait method deref
defined on Rc
, but since *box
is treated differently, it is possible to do a precise capture of the contents of the Box
.
Box
with non-move
closure
In a non-move
closure, if the contents of the Box
are not moved into the closure body, the contents of the Box
are precisely captured.
However, if the contents of the Box
are moved into the closure, then the box is entirely captured. This is done so the amount of data that needs to be moved into the closure is minimized.
Box
with move closure
Similarly to moving contents of a Box
in a non-move
closure, reading the contents of a Box
in a move
closure will capture the Box
entirely.
Unique immutable borrows in captures
Captures can occur by a special kind of borrow called a unique immutable borrow, which cannot be used anywhere else in the language and cannot be written out explicitly. It occurs when modifying the referent of a mutable reference, as in the following example:
In this case, borrowing x
mutably is not possible, because x
is not mut
.
But at the same time, borrowing x
immutably would make the assignment illegal,
because a & &mut
reference might not be unique, so it cannot safely be used to modify a value.
So a unique immutable borrow is used: it borrows x
immutably, but like a mutable borrow, it must be unique.
In the above example, uncommenting the declaration of y
will produce an error because it would violate the uniqueness of the closure’s borrow of x
; the declaration of z is valid because the closure’s lifetime has expired at the end of the block, releasing the borrow.
Call traits and coercions
Closure types all implement FnOnce
, indicating that they can be called once
by consuming ownership of the closure. Additionally, some closures implement
more specific call traits:
- A closure which does not move out of any captured variables implements
FnMut
, indicating that it can be called by mutable reference.
- A closure which does not mutate or move out of any captured variables
implements
Fn
, indicating that it can be called by shared reference.
Note:
move
closures may still implementFn
orFnMut
, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them.
Non-capturing closures are closures that don’t capture anything from their
environment. Non-async, non-capturing closures can be coerced to function pointers (e.g., fn()
)
with the matching signature.
Async closure traits
Async closures have a further restriction of whether or not they implement FnMut
or Fn
.
The Future
returned by the async closure has similar capturing characteristics as a closure. It captures place expressions from the async closure based on how they are used. The async closure is said to be lending to its Future
if it has either of the following properties:
- The
Future
includes a mutable capture. - The async closure captures by value, except when the value is accessed with a dereference projection.
If the async closure is lending to its Future
, then FnMut
and Fn
are not implemented. FnOnce
is always implemented.
Example: The first clause for a mutable capture can be illustrated with the following:
The second clause for a regular value capture can be illustrated with the following:
The exception of the the second clause can be illustrated by using a dereference, which does allow
Fn
andFnMut
to be implemented:
Async closures implement AsyncFn
, AsyncFnMut
, and AsyncFnOnce
in an analogous way as regular closures implement Fn
, FnMut
, and FnOnce
; that is, depending on the use of the captured variables in its body.
Other traits
All closure types implement Sized
. Additionally, closure types implement the
following traits if allowed to do so by the types of the captures it stores:
The rules for Send
and Sync
match those for normal struct types, while
Clone
and Copy
behave as if derived. For Clone
, the order of
cloning of the captured values is left unspecified.
Because captures are often by reference, the following general rules arise:
- A closure is
Sync
if all captured values areSync
. - A closure is
Send
if all values captured by non-unique immutable reference areSync
, and all values captured by unique immutable or mutable reference, copy, or move areSend
. - A closure is
Clone
orCopy
if it does not capture any values by unique immutable or mutable reference, and if all values it captures by copy or move areClone
orCopy
, respectively.
Drop Order
If a closure captures a field of a composite types such as structs, tuples, and enums by value, the field’s lifetime would now be tied to the closure. As a result, it is possible for disjoint fields of a composite types to be dropped at different times.
Edition 2018 and before
Closure types difference
In Edition 2018 and before, closures always capture a variable in its entirety, without its precise capture path. This means that for the example used in the Closure types section, the generated closure type would instead look something like this:
struct Closure<'a> {
rect : &'a mut Rectangle,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.rect.left_top.x += 1;
self.rect.right_bottom.x += 1;
format!("{:?}", self.rect.left_top)
}
}
and the call to f
would work as follows:
f(Closure { rect: rect });
Capture precision difference
Composite types such as structs, tuples, and enums are always captured in its entirety, not by individual fields. As a result, it may be necessary to borrow into a local variable in order to capture a single field:
If, instead, the closure were to use self.vec
directly, then it would attempt to capture self
by mutable reference. But since self.set
is already borrowed to iterate over, the code would not compile.
If the move
keyword is used, then all captures are by move or, for Copy
types, by copy, regardless of whether a borrow would work. The move
keyword is usually used to allow the closure to outlive the captured values, such as if the closure is being returned or used to spawn a new thread.
Regardless of if the data will be read by the closure, i.e. in case of wild card patterns, if a variable defined outside the closure is mentioned within the closure the variable will be captured in its entirety.
Drop order difference
As composite types are captured in their entirety, a closure which captures one of those composite types by value would drop the entire captured variable at the same time as the closure gets dropped.