- References
- Function Overloading
- Default Function Arguments
- Inline Function
- Dynamic Memory Allocation
- Placement New
- nullptr
- Namespaces
- Type Aliases
- Enum classes
- constexpr
- static_assert
- mutable Keyword
- auto Keyword
- Smart Pointers
- Basics of Classes
- Constructors
- Destructors
- Operator Overloading
- Copy Semantics
- Move Semantics
- Composition, RAII & Ownership
- Inheritance
- Polymorphism
- Abstraction
- Encapsulation
- Template
- Static Memory
- Friend Function
- this Pointer
- Function Pointer
- Lambdas and Callback Management
- Union
Lambdas and Callback Management (Anonymous Functions)

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.
Syntax & Anatomy
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"
}The Power of Captures ([ ])
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();
}
};Storage & embedded constraints
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); } );
}Relevance in Embedded/Firmware
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 hereCommon Pitfalls (Practical Tips)
Pitfall | 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::functionto avoid Heap usage.
Concept understood? Let's apply and learn for real