Question.2
A developer uses a captureless lambda to register an interrupt handler within a legacy C Hardware Abstraction Layer (HAL).
The HAL function expects a standard C function pointer for the callback.
// Legacy C API
extern "C" void register_irq(void (*cb)(int));
// C++ Implementation
register_irq([](int code) {
status_led_toggle();
log_event(code);
});Why is the compiler able to pass this lambda to a function expecting a raw pointer?
In C, callbacks are handled using Function Pointers. This works, but it's rigid—you can't easily pass "state" (variables) into the callback without messy void* casting.
Lambdas (introduced in C++11) are Function Objects that you can write inline. They solve the "State" problem elegantly.
Think of a Lambda as a Disposable Function: You write it right where you need it, it captures the variables it needs from the surrounding scope, executes, and then it's gone.
The syntax looks like this: [Captures] (Parameters) { Body }
[ ] Capture Clause: "What variables from the outside world do I need?"
( ) Parameters: "What arguments does the caller pass me?" (Just like a normal function).
{ } Body: The code to execute.
int main() {
int threshold = 50;
// A Lambda stored in a variable 'check'
// It captures 'threshold' so it can use it inside.
auto check = [threshold](int value) {
if (value > threshold) {
printf("Alert! Value %d > %d\n", value, threshold);
}
};
check(60); // Prints "Alert! Value 60 > 50"
}[ ])This is where Lambdas beat Function Pointers.
[ ] (Empty): Captures nothing. Behaves exactly like a standard C function.
[=] (By Value): Copies all used local variables into the lambda. Safe, but read-only by default.
[&] (By Reference): Accesses the actual variables. Dangerous if the lambda runs after the variable scope ends.
[this]: Captures the current object. Essential for using member variables inside a callback.
class Button {
int id = 1;
public:
void onClick() {
// Capture 'this' to access 'id' inside the lambda
auto callback = [this]() {
printf("Button %d Clicked\n", this->id);
};
callback();
}
};How do you store a Lambda? This is the tricky part in firmware.
A. The "Captureless" Optimization (Zero Cost)
If a lambda has empty captures [], the compiler treats it identical to a raw function pointer.
Cost: Zero RAM overhead. No Heap.
Use: Compatible with legacy C drivers expecting void (*ptr)(int).
// Legacy C Driver
void register_irq( void (*cb)(int) );
void setup() {
// Works perfectly! Decays to function pointer.
register_irq( [](int code) {
printf("IRQ %d\n", code);
});
}B. The "Capturing" Challenge (std::function)
If a lambda has captures (e.g., [x]), it is no longer just code; it is an object with data. It won't fit in a raw function pointer.
Solution 1 (Standard): Use std::function<void(int)>.
Warning: This often uses malloc (Heap) to store the captured variables. Risky in strict embedded.
Solution 2 (Template): The "Zero Cost" way for C++.
// Template approach: The compiler generates a custom type for the lambda.
// No Heap. No Virtual calls. Maximum speed.
template <typename Callback>
void run_task(Callback cb) {
cb();
}
int main() {
int x = 10;
// We can capture 'x' without worrying about malloc
run_task( [x](){ printf("%d", x); } );
}1. Asynchronous Event Handling
Instead of writing 10 different tiny functions (handleButton1, handleButton2), you define the logic inline during initialization.
button1.onPress( [](){ led.on(); } );
button2.onPress( [](){ motor.stop(); } );2. Custom Iterators
Running logic over a collection of sensors.
// 'sensors' is an array of objects
std::for_each(sensors.begin(), sensors.end(), [](Sensor& s) {
s.calibrate(); // Runs for every sensor
});3. Scoped Locks (RAII)
You can use a lambda to define a critical section that automatically executes logic.
execute_atomic( []() {
// Interrupts are disabled automatically by the wrapper
critical_variable++;
}); // Interrupts re-enabled herePitfall | Details |
|---|---|
❌ Dangling Reference | Capturing a local variable by reference [&] and passing the lambda to a Timer/ISR. When the Timer fires later, the function has returned, the stack is gone, and the reference points to garbage. Crash. |
❌ Size Overhead | A capturing lambda is an object. If you capture 10 integers [=], the lambda object is 40 bytes. Copying this around takes CPU cycles. |
❌ | By default, [=] captures are const (read-only). If you want to modify a captured copy inside the lambda, you must write []() mutable { ... }. |
✅ Use | Lambda types are unpronounceable compiler-generated secrets. Always use auto to hold them. |
Summary Checklist
Use [] (empty) if you need compatibility with C function pointers.
Use [this] if writing a lambda inside a class method.
Avoid [&] (reference capture) for asynchronous tasks (Timers/ISRs).
Prefer Templates over std::function to avoid Heap usage.