Lambda Expressions in C++
“Perfect is the enemy of good.” – Voltaire
https://en.wikipedia.org/wiki/Perfect_is_the_enemy_of_good

Source code for the examples can be found on github: https://github.com/mday299/keypuncher/tree/main/C%2B%2B/Intermediate/Lambda
I'm going to be honest: I did not like lambda expressions https://learn.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170 in C++ for a long time. I thought, "if there is a way to do this in a more structured way then we should be doing that." Well, that's the whole point, it turns out. When you are rushing on a project and don't have time to do it The Best Way, the Good Enough way is preferred. Sometimes it ends up being just what the doctor ordered and it stays in your permanent toolbox for a long time!
This article will cover:
- Some helpful prerequisites for lambda expressions.
- What makes lambda expressions useful and when they start to get brittle.
- Easy, Intermediate, Advanced examples of lambda usage.
- Why lambdas are good for callbacks for event-driven programming and interrupt handling.
I’m doing this in g++ on Ubuntu 24.04 and these examples should be portable anywhere. If they are not, email me please! mailto:mday299@pm.net. I am always trying to get better!
Prerequisites
These are some things you should be aware of when you are ready to dip your toe into lambda expressions.
Make sure you understand the auto in keyword in C++ https://www.w3schools.com/cpp/cpp_auto.asp.
Passing by reference vs passing by value https://github-pages.ucl.ac.uk/research-computing-with-cpp/02cpp1/sec02PassByValueOrReference.html. Be careful that your source variables don’t go out of scope when passing by reference.
Scope and lifetime
- General Scope vs Block Scope vs Namespace Scope vs Class Scope, etc., are all covered pretty well here: https://en.cppreference.com/cpp/language/scope
- Differences between stack memory and heap memory is explained here: https://www.geeksforgeeks.org/dsa/stack-vs-heap-memory-allocation/
- Dangling references and dangling pointers. https://en.wikipedia.org/wiki/Dangling_pointer, https://en.wikipedia.org/wiki/Reference_(C%2B%2B)
Template argument deduction https://en.cppreference.com/cpp/language/template_argument_deduction
Return type deduction https://www.geeksforgeeks.org/cpp/return-type-deduction-in-c14-with-examples/
Introduction
C doesn’t have lambdas, but places you might encounter lambda functions include, but are not limited to: Java (https://stackoverflow.com/questions/25192108/what-is-the-breakdown-for-javas-lambda-syntax) and even Excel and Google Sheets (https://www.vertex42.com/lambda/).
What are Lambda Expressions?
Lambdas are just unnamed function objects (or functors) https://stackoverflow.com/questions/4686507/lambda-expression-vs-functor-in-c -- see also: https://www.geeksforgeeks.org/cpp/when-to-prefer-lambda-expressions-over-functors-in-cpp/
Some things they are good at
- Inline behavior (https://www.geeksforgeeks.org/cpp/inline-functions-cpp/). Example: custom comparators for std::sort.
- Local logic. When it’s used only once, exists only within a function, and doesn’t need a name or to be reused.
- Capturing local state. Lambda expressions can store copies or references of variables that were in scope when the lambda was – created allowing the lambda to use them later. Every lambda expression generates a "hidden" class like:
struct __lambda_123 {
int x; // captured variable
int operator()() const { return x + 5; }
};
and that hidden class instance is called the closure. https://stackoverflow.com/questions/220658/the-difference-between-a-closure-and-a-lambda
What they are NOT good at
- Complex logic. If a lambda grows too large they can get difficult to read, debug, and test. At that point, prefer functors over lambda expressions.
- Multiple methods or behaviors. Lambdas can have only one call operator. If you need more structure, use a class, a struct, or a functor. They also can’t have multiple methods nor provide helper functions.
- Long-term state. Lambdas can hold long-term state if you are careful; the real issue is capturing references to short-lived objects. They should not be used to capture references that outlive their scope or used in asynchronous callbacks that capture stack variables.
- Lambdas are by definition anonymous so don’t use them if you need to document names.
In other words, they have their place, but a good rule of thumb to follow is: if they get larger than 30-50 lines a functor or class is usually better.
Example 1: Easy
Write a program that defines a vector of integers. Sort the vector in a descending order using the std::sort function and a user-provided lambda function as a predicate.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> myInts = {15,8,4,9,1,5,12};
std::sort(std::begin(myInts), std::end(myInts),
[](int x,int y) {return x >y; } );
for(const auto &el : myInts){
std::cout << el << ", ";
}
std::cout<<std::endl;
return 0;
}
Example 2: Intermediate
Write a program that has a lambda that captures two variables, one passed by reference and one passed by value. Use the mutable expression to modify the one passed by value into the lambda.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int m=0; int n=0;
//this bent my brain a little when I first saw the syntax
[&, n] (int a) mutable { m = ++n + a; }(4);
std::cout<<m<<std::endl<<n<<std::endl;
return 0;
}
Example 3: Advanced
Write a program that uses std::function to bind the lambda to a functor.
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
int i = 3; int j = 5;
//NOTE The following lambda expression captures i by val and j by reference
std::function<int (void)> f = [i, &j] { return i + j; };
//change the values of i and j:
i = 12; j = 66;
//call f and print the result
std::cout << f() << std::endl;
return 0;
}
Example: 4 Getting Fancy
Event handler callbacks and thread pools. Examples of these are available at keypuncher’s github: https://github.com/mday299/keypuncher/tree/main/C%2B%2B/Intermediate/Lambda
Note that std::cout is not thread safe for interleaved writes!! See https://stackoverflow.com/questions/14718124/how-to-easily-make-stdcout-thread-safe and https://www.geeksforgeeks.org/dsa/print-all-interleavings-of-given-two-strings/.
Other Times When Lambdas Shine
Asynchronous operations
Lambdas allow asynchronous code to reference local variables without globals. Note that the URL synchronization code has been left as an exercise for the reader.
std::string url = "https://example.com/data";
asyncFetch(url, [url](std::string result) {
std::cout << "Fetched from " << url << ": " << result << "\n";
});
Why lambdas are good for this:
Capturing the URL avoids storing it in a global or class member, the callback is defined inline where the asynchronous call is made, and it keeps logic compact and readable.
Embedded ISR-like deferred logic
In embedded systems, Interrupt Service Routines (ISRs) must be tiny (https://en.wikipedia.org/wiki/Interrupt_handler & https://stackoverflow.com/questions/3392831/what-happens-in-an-interrupt-service-routine). Deferred work is often scheduled via a callback queue. Lambdas let you capture ISR state safely (by value) and defer processing. Obviously, the ISR code is incomplete here, but the idea is:
volatile uint16_t adcValue = 0;
// ISR: capture the value and queue a lambda for deferred processing
void ADC_IRQHandler() {
uint16_t sample = adcValue; // read volatile
deferQueue.push([sample]() {
// Safe: sample is captured by value
processSample(sample);
});
}
Why lambdas are great for this:
Capturing by value avoids dangling references, with no need to define a dedicated functor type for each ISR, while keeping ISR minimal and allowing flexible deferred logic.
Conclusion
You have now learned some basic and intermediate lambda expressions!
Credits
Lambda expressions https://en.cppreference.com/cpp/language/lambda
Never Nester - https://www.youtube.com/watch?v=CFRhGnuXG-4
Modern C++ for Absolute Beginners - https://learning.oreilly.com/library/view/modern-c-for/9781484292747/
