Advanced Type Features
Closure Types
Every closure expression (|args| body) in Rust causes the compiler to synthesize a unique, anonymous struct type that stores the closure's captured variables as fields and implements one or more of the closure traits (FnOnce, FnMut, Fn). Even two identical closures written on adjacent lines have distinct anonymous types (E0308).
How the Compiler Desugars a Closure into a Struct
Source Closure:
let factor: u64 = 10;
let mut calls: u32 = 0;
let mut scale = |x: u64| { calls += 1; x * factor };
Compiler-Synthesized Anonymous Struct (16 bytes on 64-bit targets):
struct __ClosureEnv<'a, 'b> {
factor_ref: &'a u64, // Shared borrow (8 bytes)
calls_ref: &'b mut u32, // Mutable borrow (8 bytes)
}
impl FnOnce<(u64,)> for __ClosureEnv<'_, '_> { ... }
impl FnMut<(u64,)> for __ClosureEnv<'_, '_> { ... }
Because each closure type is anonymous and cannot be written directly in source code, functions accept or return closures using:
- Static dispatch:
impl Fn(u64) -> u64or generic<F: Fn(u64) -> u64>. - Dynamic dispatch:
&dyn Fn(u64) -> u64orBox<dyn Fn(u64) -> u64>. - Function pointer coercion (only when the closure captures zero variables):
fn(u64) -> u64.
Memory Size and Capture Modes of Closure Types
A closure's byte size (size_of_val(&closure)) equals the sum of the fields it captures (plus alignment padding):
| Closure Capture Mode | What the Anonymous Closure Struct Stores | Size (size_of_val) |
Trait Hierarchy Implemented |
|---|---|---|---|
Non-capturing (|x| x + 1) |
Zero fields (ZST, coerces to fn pointer) |
0 bytes |
Fn + FnMut + FnOnce + Copy + Clone |
By shared reference (|x| x + factor) |
&factor per read-only captured variable |
8 bytes per reference |
Fn + FnMut + FnOnce + Copy + Clone |
By mutable reference (|x| { count += 1; x }) |
&mut count per mutated variable |
8 bytes per reference |
FnMut + FnOnce (not Fn, not Copy) |
By value (move or consumed) |
The owned values (String, Vec, u64) directly inline |
Sum of moved values | Fn / FnMut / FnOnce depending on whether body moves out of captures |
fn main() {
let stateless = |x: i32| x + 1;
let multiplier: u64 = 5;
let borrows_imm = |x: u64| x * multiplier;
let owned_prefix = String::from("tag:");
let moves_string = move |suffix: &str| format!("{}{}", owned_prefix, suffix);
println!("size of stateless closure: {} bytes", size_of_val(&stateless));
println!("size of borrows_imm closure: {} bytes", size_of_val(&borrows_imm));
println!("size of moves_string closure: {} bytes", size_of_val(&moves_string));
// A closure that only captures shared references or `Copy` values is itself `Copy`!
let copy_of_borrows = borrows_imm;
println!("borrows_imm(10) = {}, copy(10) = {}", borrows_imm(10), copy_of_borrows(10));
println!("moves_string: {}", moves_string("ok"));
}
size of stateless closure: 0 bytes
size of borrows_imm closure: 8 bytes
size of moves_string closure: 24 bytes
borrows_imm(10) = 50, copy(10) = 50
moves_string: tag:ok
Auto-Trait Propagation on Closure Types (Copy, Clone, Send, Sync)
The compiler automatically implements Copy, Clone, Send, and Sync for a closure's anonymous struct based on its captured fields:
- If all captured values (or captured
&Treferences) implementCopy(orClone), the closure itself implementsCopy(orClone)! (Note that&mut Tis notCopyorClone, so a closure capturing&mut Tis neitherCopynorClone.) - If all captured values/references are
Send(andSync), the closure itself implementsSend(andSync).