Function Pointers vs Closures

Closures and Fn Traits covered the Fn/FnMut/FnOnce traits closures implement. Sitting alongside them is a distinct, older thing: the function pointer type fn(T) -> U, a plain pointer to a named function, with no captured environment. The two are easy to conflate because both are "something callable you can pass around," but they have different capabilities and costs, and knowing when a fn pointer suffices (versus needing a closure) matters for FFI, enums, and clean APIs. This tutorial covers the distinction, the coercion that connects them, and where each fits.


Two Kinds of "Callable"

A function pointer fn(i32) -> i32 is a pointer to a specific named function. It carries no state, just the code address, so it's a single machine word and implements Copy. A closure is an anonymous struct bundling captured variables plus code, and its type is unnameable (Closures and Fn Traits).

fn double(x: i32) -> i32 { x * 2 }

// function pointer: names an existing fn, captures nothing
let f: fn(i32) -> i32 = double;
println!("{}", f(5));   // 10

// closure: can capture from its environment
let factor = 3;
let g = move |x: i32| x * factor;   // captures `factor` — not a plain fn pointer
println!("{}", g(5));   // 15

The defining difference is capture: double can be a fn pointer because it references nothing external; g captures factor, so it needs the closure machinery to carry that value. A fn pointer is the strictly simpler, less capable of the two.


Non-Capturing Closures Coerce to fn Pointers

The bridge between the two: a closure that captures nothing automatically coerces to a fn pointer. This is what lets you pass either a named function or a simple inline closure to an API expecting fn:

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

apply(double, 5);            // a named function
apply(|x| x + 1, 5);         // a NON-capturing closure — coerces to fn pointer

Gotcha: the coercion only works for closures that capture nothing. The moment a closure captures a variable, it is not a fn pointer and won't coerce, apply(|x| x + factor, 5) fails to compile because that closure carries factor. The error ("expected fn pointer, found closure") is confusing until you realize capturing is the dividing line. If an API takes fn(...) and you need to capture, that API can't accept your closure, either restructure to avoid capture, or the API should take impl Fn(...) / Box<dyn Fn(...)> instead (which accept both). When designing your own APIs, prefer impl Fn over fn unless you specifically need a bare pointer.


Where fn Pointers Are the Right Choice

Given closures are more capable, why ever use a fn pointer? Three real cases:

// 1. FFI callbacks — C expects a plain function pointer, not a Rust closure
extern "C" {
    fn register_handler(cb: extern "C" fn(i32));
}

// 2. Storing a function in a struct/enum without generics or boxing
enum Operation {
    Unary(fn(i32) -> i32),        // a plain fn pointer field — Copy, no lifetime
}
let op = Operation::Unary(double);

// 3. A lookup table of named operations
let ops: [(&str, fn(i32, i32) -> i32); 2] = [
    ("add", |a, b| a + b),
    ("sub", |a, b| a - b),
];

fn pointers shine when you need a callable that's Copy, has no lifetime, and needs no heap allocation, exactly what FFI requires (C can only call a bare function pointer, never a Rust closure with captured state), and what makes storing one in an enum variant or a const table clean. Because a fn pointer carries no environment, it sidesteps the "unnameable type" problem: you can write fn(i32) -> i32 as a struct field type directly, whereas storing a closure needs a generic parameter or Box<dyn Fn>.


Choosing Between Them

SituationReach for
Callable that captures nothing, name it as a typefn(T) -> U pointer
FFI callback into/out of Cextern "C" fn(...) pointer
Store a callable in an enum/struct field simplyfn pointer field (Copy, no generics)
Callable that must capture environmenta closure (impl Fn / Box<dyn Fn>)
API parameter accepting either fn or closureimpl Fn(...) (accepts both, incl. capturing)
Store closures of varying origin togetherBox<dyn Fn(...)>

The rule of thumb: accept impl Fn(...) in your APIs by default, it's the most permissive, taking named functions, non-capturing closures, and capturing closures alike. Use a bare fn(...) pointer specifically when you need the properties a closure can't give cheaply: Copy, no lifetime, no allocation, a nameable type for a struct field, or FFI compatibility. Since a non-capturing closure coerces to fn, an API taking impl Fn never loses callers by being generic, so fn is a deliberate narrowing, not a default.


Key Takeaways

  • A function pointer fn(T) -> U points to a named function and carries no environment; it's Copy, word-sized, and has a nameable type. A closure bundles captured state with code and has an unnameable type.
  • Capture is the dividing line: a closure that captures nothing coerces to a fn pointer; the instant it captures a variable, it does not, producing the "expected fn pointer, found closure" error.
  • Use fn pointers where a closure can't fit cheaply: FFI callbacks (C needs a bare pointer), enum/struct fields storing a callable without generics or boxing, and const lookup tables.
  • Prefer impl Fn(...) in your own API parameters, it accepts named functions, non-capturing closures, and capturing closures; a bare fn(...) is a deliberate narrowing for the cases above.
  • A non-capturing closure coercing to fn means an impl Fn API never loses callers by being generic; reach for fn only when you specifically need its extra guarantees.