| |

Rust 74 🦀 Function Pointers (fn) vs Closure Types

⏱️ Estimated read: 16 min | |

Rust has two distinct mechanisms for representing callable code: function pointers (fn) and closures. They look similar at the call site—both can be invoked with parentheses—but they are fundamentally different types with different capabilities, memory representations, and trait implementations. A function pointer is a pointer to a function’s machine code. It is Copy, has a known size (one word), and carries no captured state. A closure is an anonymous struct that holds the captured variables from its environment and implements one or more of the Fn, FnMut, or FnOnce traits. It may be zero-sized if it captures nothing, or it may carry arbitrary data if it captures by value or by reference.

The confusion between them arises because a non-capturing closure can be coerced to a function pointer. The compiler sees that the closure does not need an environment, generates a function for it, and allows let f: fn(i32) -> i32 = |x| x + 1; . But a capturing closure cannot be coerced, because the function pointer has no place to store the captured values. This is the core distinction: function pointers are stateless, closures are stateful. The choice between them is determined by whether the callable needs to remember anything from its creation environment.

Key point: fn is a function pointer type—a stateless, Copy pointer to machine code. Closures are anonymous structs that implement Fn, FnMut, or FnOnce, carrying captured state. Non-capturing closures coerce to fn; capturing closures do not. Generic functions accepting F: Fn() are monomorphized for each closure type, while fn parameters use a single instantiation and an indirect call.


Why both types exist

The zero-cost abstraction problem. Closures in Rust are designed to be inlined. When you pass a closure to a generic function like Iterator::map, the compiler monomorphizes the function for that specific closure type, knows the exact code that runs, and inlines it. There is no function call overhead, no boxing, no dynamic dispatch. This is the “zero-cost abstraction” that Rust promises: the closure is as fast as writing the loop by hand . A function pointer, by contrast, requires an indirect call through the pointer, which the compiler cannot inline because it does not know the target at compile time.

The stateless callback problem. Sometimes the callback genuinely has no state. A comparison function for sorting, a mathematical operation, a simple transformation—these do not need to capture anything. A function pointer is the correct type for these: it is small, Copy, and can be stored in a struct without a lifetime parameter or a generic. A struct Config { on_error: fn(&str) } is simpler than struct Config<F: Fn(&str)> { on_error: F } because the former has no generic parameter to thread through every use of Config .

The FFI problem. C function pointers are a fixed ABI: a pointer to machine code with a specific calling convention. Rust’s fn type maps directly to that representation. extern "C" fn(i32) -> i32 is the type that can be passed to a C library. A closure cannot, because the C ABI has no concept of a captured environment. The fn type exists partly to make FFI possible .

The trait object problem. A Box<dyn Fn(i32) -> i32> can hold any closure with that signature, capturing or not. It has a vtable and a heap allocation. A fn(i32) -> i32 is a single pointer with no allocation. When the set of possible callbacks is fixed and known at compile time, fn is cheaper. When the callbacks are provided at runtime and may capture state, Box<dyn Fn> is the flexible choice. The distinction is the same as the general one between generics and trait objects: monomorphization versus dynamic dispatch.


a. Function pointers: fn as a type

The fn type is a function pointer. It is written with the parameter and return types in parentheses, like a function signature without the name.

// A function that doubles an integer
fn double(x: i32) -> i32 {
    x * 2
}

// A function that adds one
fn increment(x: i32) -> i32 {
    x + 1
}

// The parameter type is fn(i32) -> i32: a function pointer
// The function applies the given operation to the value
fn apply(f: fn(i32) -> i32, value: i32) -> i32 {
    // Call the function pointer
    f(value)
}

fn main() {
    // Pass the function by name
    // The compiler coerces the function item to a fn pointer
    let result = apply(double, 5);
    assert_eq!(result, 10);

    // Function pointers are values, so they can be stored
    let operation: fn(i32) -> i32 = increment;
    assert_eq!(operation(5), 6);

    // Function pointers are Copy, so they can be passed multiple times
    let result2 = apply(operation, 5);
    assert_eq!(result2, 6);
}

The fn(i32) -> i32 type is a pointer, not a function. It has a known size (one word on most platforms), it is Copy, and it implements Fn, FnMut, and FnOnce . The function item double—the name of the function before it is coerced—is a zero-sized type that also implements those traits. The coercion from the function item to the function pointer happens automatically when the expected type is fn .

The fn type implements all three closure traits. That means a fn can be passed to any function that expects F: Fn(i32) -> i32, F: FnMut(i32) -> i32, or F: FnOnce(i32) -> i32. This is why a function pointer is a valid argument wherever a closure is expected, as long as the signature matches .

// A generic function that accepts any Fn
fn apply_generic<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
    f(value)
}

fn main() {
    // A fn pointer works because fn implements Fn
    let result = apply_generic(double, 5);
    assert_eq!(result, 10);

    // A non-capturing closure also works
    let result = apply_generic(|x| x * 3, 5);
    assert_eq!(result, 15);
}

The reverse is not true. A generic function that accepts F: Fn cannot be passed a fn only if the fn does not implement Fn—but it does, so the fn is always valid. The distinction matters for the caller’s flexibility, not for the callee’s requirements.


b. Closures: anonymous structs with captured state

A closure is an anonymous struct that implements one of the three closure traits. The trait it implements is determined by how it uses its captures: by reference (Fn), by mutable reference (FnMut), or by value (FnOnce).

fn main() {
    // A non-capturing closure
    // It implements Fn, FnMut, and FnOnce
    // It also coerces to fn(i32) -> i32
    let add_one = |x: i32| x + 1;
    assert_eq!(add_one(5), 6);

    // A capturing closure: captures `offset` by reference
    // It implements Fn because it only reads the captured value
    let offset = 10;
    let add_offset = |x: i32| x + offset;
    assert_eq!(add_offset(5), 15);

    // A capturing closure: captures `count` by mutable reference
    // It implements FnMut because it modifies the captured value
    let mut count = 0;
    let mut increment_count = || {
        count += 1;
        count
    };
    assert_eq!(increment_count(), 1);
    assert_eq!(increment_count(), 2);

    // A capturing closure: captures `message` by value
    // It implements FnOnce because it consumes the captured value
    let message = String::from("hello");
    let consume = || {
        // Move the string out of the closure
        drop(message);
    };
    consume();
    // consume(); // error: FnOnce can only be called once
}

The closure trait that the compiler infers is the most restrictive one that matches the closure’s behavior. A closure that only reads its captures implements Fn, FnMut, and FnOnce. A closure that mutates its captures implements FnMut and FnOnce but not Fn. A closure that consumes its captures implements only FnOnce . The hierarchy is: every Fn is FnMut and FnOnce; every FnMut is FnOnce; not every FnOnce is FnMut or Fn.

The move keyword forces the closure to take ownership of its captures. This is necessary when the closure outlives the scope of the captured variables—for example, when it is returned from a function or passed to a thread.

// A function that returns a closure
// The closure captures `prefix` by reference, so it cannot outlive it
// The move keyword forces ownership, allowing the closure to be returned
fn make_greeter(prefix: String) -> impl Fn(&str) -> String {
    move |name| format!("{}, {}!", prefix, name)
}

fn main() {
    // The greeter owns its prefix, so it is valid after the function returns
    let greeter = make_greeter(String::from("Hello"));
    assert_eq!(greeter("Alice"), "Hello, Alice!");
}

Without move, the closure would borrow prefix, and the borrow would end when make_greeter returns. The move keyword makes the closure own prefix, so the closure is valid as long as it is used .


c. The coercion and the generic trade-off

A non-capturing closure coerces to a function pointer. The compiler detects that the closure does not capture anything, generates a function for it, and allows the coercion. This is why the following code compiles:

fn main() {
    // A non-capturing closure coerces to a fn pointer
    let f: fn(i32) -> i32 = |x| x + 1;
    assert_eq!(f(5), 6);

    // A capturing closure does not coerce
    let offset = 10;
    // let g: fn(i32) -> i32 = |x| x + offset; // error: captures
}

The coercion is useful when a fn pointer is required—for example, by an FFI API, by a struct that stores a function pointer, or by a function that accepts fn as a parameter. The non-capturing closure is a convenient way to write the function inline without declaring a named function.

The generic trade-off is the performance and code-size decision. A generic function fn apply<F: Fn(i32) -> i32>(f: F) is monomorphized for every concrete F. Each closure type produces a separate copy of apply. The call is direct and inlinable. A function that takes fn(i32) -> i32 has a single instantiation, but the call is indirect and cannot be inlined .

AspectGeneric F: Fnfn pointer
DispatchStatic (monomorphized)Dynamic (indirect call)
InliningYesNo
Code sizeLarger (one copy per closure)Smaller (one copy)
CapturesYesNo
CoercionNon-capturing closure coercesOnly non-capturing

The choice is the same as the general one between generics and trait objects: use generics when performance matters and the set of types is known at compile time; use fn or Box<dyn Fn> when the set of callbacks is determined at runtime or when the code size is a concern.


Complete Example Session

The following session builds a small event system with a function pointer for a static handler and a closure for a stateful handler, demonstrating the coercion and the trait bounds.

// ============================================
// PART 1: THE FUNCTION POINTER HANDLER
// ============================================
// A function that logs an event
fn log_event(event: &str) {
    println!("[LOG] {}", event);
}

// A function that counts events
fn count_event(event: &str) {
    println!("[COUNT] {}", event);
}

// A struct that stores a function pointer as a handler
// No generic parameter, no lifetime parameter
struct StaticHandler {
    // The handler is a fn pointer
    // It cannot capture state
    handler: fn(&str),
}

impl StaticHandler {
    // Create a new handler from a function pointer
    fn new(handler: fn(&str)) -> Self {
        StaticHandler { handler }
    }

    // Dispatch the event to the handler
    fn handle(&self, event: &str) {
        // Call the function pointer
        (self.handler)(event);
    }
}

// ============================================
// PART 2: THE CLOSURE HANDLER
// ============================================
// A struct that stores a closure as a handler
// The generic parameter F is the closure type
// The handler can capture state
struct DynamicHandler<F: Fn(&str)> {
    // The handler is the closure
    handler: F,
}

impl<F: Fn(&str)> DynamicHandler<F> {
    // Create a new handler from a closure
    fn new(handler: F) -> Self {
        DynamicHandler { handler }
    }

    // Dispatch the event to the handler
    fn handle(&self, event: &str) {
        // Call the closure
        (self.handler)(event);
    }
}

// ============================================
// PART 3: THE MAIN FUNCTION
// ============================================
fn main() {
    // A static handler: no state, fn pointer
    let static_handler = StaticHandler::new(log_event);
    static_handler.handle("user logged in");

    // A dynamic handler: captures a prefix
    let prefix = String::from("AUDIT");
    let dynamic_handler = DynamicHandler::new(|event: &str| {
        // The closure captures `prefix` by reference
        println!("[{}] {}", prefix, event);
    });
    dynamic_handler.handle("user logged in");

    // A non-capturing closure coerces to a fn pointer
    let fn_pointer: fn(&str) = |event| println!("[INLINE] {}", event);
    let coerced_handler = StaticHandler::new(fn_pointer);
    coerced_handler.handle("coerced event");

    // A capturing closure does not coerce
    // let bad: fn(&str) = |event| println!("[{}] {}", prefix, event);
    // error: closure captures `prefix` by reference
}

The three parts cover the function pointer handler with no state, the closure handler with captured state, and the main function that demonstrates the coercion of a non-capturing closure and the rejection of a capturing one. What the session demonstrates is that the function pointer struct has no generic parameter and is simpler to use, while the closure struct requires a generic parameter but can carry state. The choice is determined by whether the handler needs to remember anything from its creation context.


Quick Reference

Function Pointer vs Closure

AspectfnClosure
TypePointer to codeAnonymous struct
SizeOne wordZero-sized or captured data
CopyYesOnly if captures are Copy
Captures stateNoYes
Trait implsFn, FnMut, FnOnceFn, FnMut, or FnOnce
CoercionFrom non-capturing closureTo fn if non-capturing
DispatchIndirectDirect (monomorphized)
InliningNoYes (generic context)

Closure Trait Hierarchy

TraitCapturesCallable
FnBy referenceMultiple times
FnMutBy mutable referenceMultiple times
FnOnceBy valueOnce

Coercion Rules

ClosureCoerces to fn?
Non-capturingYes
Captures by referenceNo
Captures by mutable referenceNo
Captures by valueNo

Generic vs fn Parameter

AspectF: Fn()fn()
MonomorphizationPer closure typeSingle
Code sizeLargerSmaller
InliningYesNo
Accepts capturing closuresYesNo

Best Practices

✅ Do This:

// Use fn for stateless callbacks stored in a struct
struct Config { on_error: fn(&str) }

// Use generics for hot-path callbacks that benefit from inlining
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }

// Use move when the closure outlives its captures
fn make(prefix: String) -> impl Fn(&str) -> String {
    move |name| format!("{}, {}", prefix, name)
}

// Use Box<dyn Fn> when the callback set is dynamic
let handlers: Vec<Box<dyn Fn(&str)>> = vec![Box::new(|s| println!("{}", s))];

❌ Don’t Do This:

// Try to coerce a capturing closure to fn
let offset = 10;
let f: fn(i32) -> i32 = |x| x + offset; // error: captures

// Use a fn pointer in a hot loop when a generic would inline
fn apply(f: fn(i32) -> i32, x: i32) -> i32 { f(x) } // indirect call

// Forget move when returning a closure that captures
fn make(s: String) -> impl Fn() { || println!("{}", s) } // error: borrow

Common Pitfalls

PitfallWhy It HappensFix
Capturing closure does not coercefn has no environmentUse generic or Box<dyn Fn>
Closure outlives captureBorrow without moveAdd move
FnOnce called twiceConsumes capturesUse Fn or FnMut
fn pointer not inlinedIndirect callUse generic F: Fn
Closure type is unnameableAnonymous typeUse impl Fn or Box<dyn Fn>

Real-World Examples

1. Function pointer parameter

fn apply(f: fn(i32) -> i32, x: i32) -> i32 { f(x) }

2. Non-capturing closure coercion

let f: fn(i32) -> i32 = |x| x + 1;

3. Capturing closure

let offset = 10;
let f = |x: i32| x + offset;

4. move closure

let s = String::from("hello");
let f = move || println!("{}", s);

5. Generic Fn parameter

fn call<F: Fn()>(f: F) { f(); }

6. Box<dyn Fn>

let handlers: Vec<Box<dyn Fn(i32) -> i32>> = vec![Box::new(|x| x + 1)];

7. Struct field with fn

struct Config { on_error: fn(&str) }

8. Struct field with generic closure

struct Handler<F: Fn(&str)> { f: F }

9. FnMut closure

let mut count = 0;
let mut inc = || { count += 1; };
inc();

10. FnOnce closure

let s = String::from("hello");
let consume = move || drop(s);
consume();

Visual

Function Pointer

┌──────────────────────────────────────────────┐
│  fn(i32) -> i32                              │
│  ┌─────────────────┐                         │
│  │ address of code │  one word               │
│  └─────────────────┘                         │
│         │                                    │
│         ▼                                    │
│  ┌─────────────────┐                         │
│  │ machine code    │                         │
│  └─────────────────┘                         │
│                                              │
│  No captured state                           │
│  Copy, Sized                                 │
└──────────────────────────────────────────────┘

Closure

┌──────────────────────────────────────────────┐
│  |x| x + offset                              │
│  ┌─────────────────┬─────────────────┐       │
│  │ code address    │ captured offset │       │
│  └─────────────────┴─────────────────┘       │
│                                              │
│  Anonymous struct                            │
│  Size depends on captures                    │
│  Not necessarily Copy                        │
└──────────────────────────────────────────────┘

Coercion

┌──────────────────────────────────────────────┐
│  NON-CAPTURING CLOSURE                       │
│  |x| x + 1                                   │
│         │                                    │
│         ▼                                    │
│  Coerces to fn(i32) -> i32                   │
│                                              │
│  CAPTURING CLOSURE                           │
│  |x| x + offset                              │
│         │                                    │
│         ▼                                    │
│  Does NOT coerce to fn                       │
│  (no place to store offset)                  │
└──────────────────────────────────────────────┘

Generic vs fn Parameter

┌──────────────────────────────────────────────┐
│  GENERIC F: Fn(i32) -> i32                   │
│  → monomorphized per closure type            │
│  → direct call, inlinable                    │
│  → accepts capturing closures                │
│                                              │
│  fn(i32) -> i32                              │
│  → single instantiation                      │
│  → indirect call, not inlinable              │
│  → rejects capturing closures                │
└──────────────────────────────────────────────┘

Summary

ItemValue
fn typeFunction pointer
fn sizeOne word
fn is CopyYes
fn captures stateNo
Closure typeAnonymous struct
Closure capturesBy reference, mutable reference, or value
Closure traitsFn, FnMut, FnOnce
CoercionNon-capturing closure → fn
Generic dispatchMonomorphized, inlinable
fn dispatchIndirect, not inlinable

Key takeaways:

  • fn is a pointer; a closure is a struct. The function pointer holds an address of machine code and nothing else. The closure holds the captured variables and implements a call trait .
  • Non-capturing closures coerce to fn. The compiler generates a function for the closure and allows the coercion. Capturing closures cannot coerce because the function pointer has no place to store the captured values .
  • fn implements all three closure traits. A fn can be passed to any function that expects Fn, FnMut, or FnOnce. The reverse is not true: a capturing closure cannot be passed to a function that expects fn .
  • The closure trait hierarchy is Fn → FnMut → FnOnce. A closure that only reads its captures implements all three. A closure that mutates implements FnMut and FnOnce. A closure that consumes implements only FnOnce .
  • move forces ownership of captures. Without it, the closure borrows the captured variables, and the borrow must outlive the closure. With it, the closure owns the values and can outlive the scope where they were defined .
  • Generic F: Fn is faster than fn. The generic is monomorphized for each closure type, allowing direct calls and inlining. The fn pointer requires an indirect call that the compiler cannot inline .

Remember: Function pointers and closures are both callable, but they are not interchangeable. A function pointer is a stateless pointer to code. A closure is a struct that carries captured state and implements a call trait. The non-capturing closure is the bridge between them: it coerces to fn because it has no state to carry. The choice between fn and a generic F: Fn is the choice between dynamic dispatch and monomorphization—the same trade-off as trait objects versus generics, applied to callables. Use fn when the callback is stateless and stored in a struct, or when the ABI requires it. Use generics when the callback is in a hot path and the set of closures is known at compile time. Use Box<dyn Fn> when the set of callbacks is determined at runtime and may capture state.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!