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
fnpointer and won't coerce,apply(|x| x + factor, 5)fails to compile because that closure carriesfactor. The error ("expected fn pointer, found closure") is confusing until you realize capturing is the dividing line. If an API takesfn(...)and you need to capture, that API can't accept your closure, either restructure to avoid capture, or the API should takeimpl Fn(...)/Box<dyn Fn(...)>instead (which accept both). When designing your own APIs, preferimpl Fnoverfnunless 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
| Situation | Reach for |
|---|---|
| Callable that captures nothing, name it as a type | fn(T) -> U pointer |
| FFI callback into/out of C | extern "C" fn(...) pointer |
| Store a callable in an enum/struct field simply | fn pointer field (Copy, no generics) |
| Callable that must capture environment | a closure (impl Fn / Box<dyn Fn>) |
| API parameter accepting either fn or closure | impl Fn(...) (accepts both, incl. capturing) |
| Store closures of varying origin together | Box<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) -> Upoints to a named function and carries no environment; it'sCopy, 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
fnpointer; the instant it captures a variable, it does not, producing the "expected fn pointer, found closure" error. - Use
fnpointers where a closure can't fit cheaply: FFI callbacks (C needs a bare pointer), enum/struct fields storing a callable without generics or boxing, andconstlookup tables. - Prefer
impl Fn(...)in your own API parameters, it accepts named functions, non-capturing closures, and capturing closures; a barefn(...)is a deliberate narrowing for the cases above. - A non-capturing closure coercing to
fnmeans animpl FnAPI never loses callers by being generic; reach forfnonly when you specifically need its extra guarantees.