Table of Contents
Storing a closure in a struct requires specifying trait bounds (Fn, FnMut, FnOnce) and potentially lifetimes if the closure captures references. Here's how to do it:
1. Generic Struct (Static Dispatch)
Use a generic type parameter with Fn/FnMut/FnOnce bounds. Ideal for fixed closure types.
Example: Fn Trait
struct Processor<F>
where
F: Fn(i32) -> i32, // Trait bound for closure type
{
operation: F,
value: i32,
}
impl<F> Processor<F>
where
F: Fn(i32) -> i32,
{
fn run(&self) -> i32 {
(self.operation)(self.value)
}
}
fn main() {
let adder = Processor {
operation: |x| x + 5, // Closure captured by value
value: 10,
};
println!("{}", adder.run()); // 15
}
Key Points
- Zero runtime overhead: Monomorphized for each closure type.
- Fixed closure type: Can't store different closures in the same struct.
2. Trait Object (Dynamic Dispatch)
Use Box
Example: Box
struct DynamicProcessor<'a> {
operation: Box<dyn Fn(i32) -> i32 + 'a>, // Trait object with optional lifetime
value: i32,
}
impl<'a> DynamicProcessor<'a> {
fn run(&self) -> i32 {
(self.operation)(self.value)
}
}
fn main() {
let multiplier = 2;
let processor = DynamicProcessor {
operation: Box::new(|x| x * multiplier), // Captures `multiplier`
value: 10,
};
println!("{}", processor.run()); // 20
}
Key Points
- Lifetime annotation: Required if the closure captures references (e.g., Box<dyn Fn() -> &str + 'a>).
- Flexibility: Can store any closure matching the trait.
- Overhead: Vtable lookup (dynamic dispatch).
3. Capturing References (Lifetimes)
If the closure captures references, the struct must declare lifetimes to ensure validity:
struct RefProcessor<'a, F>
where
F: Fn(&'a str) -> &'a str, // Lifetime tied to input/output
{
process: F,
data: &'a str,
}
fn main() {
let data = "hello";
let processor = RefProcessor {
process: |s| &s[1..], // Captures nothing, but input/output tied to `data`
data,
};
println!("{}", (processor.process)(processor.data)); // "ello"
}
When to Use Each
Approach | Use Case | Trade-Offs |
---|---|---|
Generic (impl Fn) | High performance, fixed closure type | Less flexible, binary bloat |
Trait Object | Dynamic behavior, multiple closures | Runtime overhead, heap allocation |
Lifetime Annotated | Closures capturing references | Ensures safety, adds complexity |
Key Takeaways
✅ Generic structs: Best for performance and static dispatch. ✅ Trait objects: Use when storing heterogeneous closures. ✅ Lifetimes: Required if the closure captures references.
Try This: What happens if a closure captures a &mut reference and is stored in a struct?
Answer: The struct must be mut, and the closure must implement FnMut!
Back to Blog
Share: