Closures

Capturing Environment Variables

When a closure references variables from its enclosing scope, the compiler synthesizes an anonymous struct holding those captured variables as fields and chooses the least restrictive capture mode needed by the closure body.

The Three Capture Modes

By default (without the move keyword), the compiler inspects how each outer variable is used inside the closure body and selects one of three capture modes in order of preference:

Priority Capture Mode Stored in Closure Struct Triggered When the Closure Body...
1 (Least restrictive) Shared reference (&T) &'a T Only reads the variable (println!("{x}"), x.len())
2 Mutable reference (&mut T) &'a mut T Mutates the variable (x += 1, vec.push(v))
3 (Most restrictive) By value (T) T (moved or copied) Moves ownership out of the variable (drop(x), into_iter(), returning x)
Enclosing Stack Frame                Synthesized Closure Struct
+---------------------+              +--------------------------+
| threshold: i32 = 10 | <─────────── | threshold_ref: &i32      | (Shared &T)
| count: usize = 0    | <─────────── | count_ref: &mut usize    | (Mutable &mut T)
| tag: String         | ──(moved)──> | tag: String              | (By value T)
+---------------------+              +--------------------------+

Disjoint Field Capture (Rust 2021 and 2024 Editions)

Starting in the 2021 edition and continuing in the 2024 edition, closures capture individual struct fields (places) rather than always capturing the entire struct. If a closure only reads config.port, the remaining fields of config (such as config.host) can still be mutated or moved independently while the closure is alive:

struct ServerState {
    host: String,
    active_connections: u32,
}

fn main() {
    let mut state = ServerState {
        host: String::from("localhost"),
        active_connections: 0,
    };

    // Captures ONLY `&state.host` by shared reference:
    let print_host = || println!("Host: {}", state.host);

    // Mutating `state.active_connections` is allowed while `print_host` is alive:
    state.active_connections += 1;

    print_host();
    println!("Connections: {}", state.active_connections);
}
Host: localhost
Connections: 1

Memory Size of a Closure

A closure's size on the stack (std::mem::size_of_val) equals the sum of its captured fields (plus alignment padding):

use std::mem::size_of_val;

fn main() {
    let a: u64 = 10;
    let b: [u8; 32] = [0; 32];

    let no_capture = || 42;
    let borrow_a = || a;
    let move_b = move || b[0];

    println!("no_capture: {} bytes", size_of_val(&no_capture));
    println!("borrow_a:   {} bytes (1 pointer)", size_of_val(&borrow_a));
    println!("move_b:     {} bytes ([u8; 32] by value)", size_of_val(&move_b));
}
no_capture: 0 bytes
borrow_a:   8 bytes (1 pointer)
move_b:     32 bytes ([u8; 32] by value)