Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
C++ Lesson 10 — Lambdas
Lambda syntax, capture by value vs reference, mutable and generic lambdas, init-capture, the anonymous class lambdas desugar to, and the function-pointer decay rule.
- What you'll learn
- Lambda syntax and captures, mutable lambdas, generic lambdas, init-capture, lambda as function object, function-pointer decay for non-capturing lambdas
- Code provided
- About 130 lines — comparator, event handler, and the equivalent hand-written class
- Time
- 45-60 minutes
- Pre-reqs
- Lessons 1-9 (templates, STL, smart pointers, move semantics)
Lambdas are the syntax that finally made C++ pleasant to use with STL algorithms. Before C++11, sorting a std::vector of structs by a member meant writing a named comparison functor — twenty lines of class boilerplate to wrap a one-line comparison. With lambdas you write the comparison inline at the call site and the compiler builds the functor for you. The mechanism is straightforward once you see what the compiler is actually generating: a lambda is an anonymous class with an operator(). That's it.
This lesson covers the syntax in full and shows the desugared equivalent so the magic stops being magic.
Syntax
The full form:
[capture] (parameters) -> return-type { body }Most of those parts are optional. Minimal lambda:
auto greet = [] { std::println("hello"); };
greet(); // helloLambda with parameters and an inferred return type:
auto add = [](int a, int b) { return a + b; };
int s = add(2, 3); // 5Lambda with an explicit return type — needed only when the compiler cannot deduce or the deduction differs from what you want:
auto half = [](int x) -> double { return x / 2.0; };The type of a lambda is a unique, unnamed class type generated by the compiler. You typically store it in auto or pass it to a function template that deduces the type. decltype(half) is some compiler-internal name like main()::{lambda(int)#1}. You cannot spell it directly.
Captures: by value, by reference, by default
The capture clause [...] lists what enclosing variables the lambda can see and how. The four common forms:
int x = 10, y = 20;
auto by_value = [x, y] { return x + y; }; // x and y copied at lambda creation
auto by_reference = [&x, &y] { return x + y; }; // x and y referred to live
auto value_all = [=] { return x + y; }; // every used variable captured by value
auto ref_all = [&] { return x + y; }; // every used variable captured by reference
x = 999;
std::println("{} {} {} {}", by_value(), by_reference(), value_all(), ref_all());
// 30 1019 30 1019[=] and [&] capture only the variables actually used in the body. They do not, despite the syntax, "capture everything" — unused outer variables are not included.
You can mix: [=, &counter] captures everything by value except counter, which is captured by reference. [&, total] is the inverse.
Capture of this
Member-function lambdas often need to access member variables. The captures [this] and [*this] cover this:
class Widget {
public:
void start() {
on_event([this] { ++count_; }); // captures this pointer
on_event([*this] mutable { ++count_; }); // captures a copy of *this
}
private:
int count_ = 0;
};[this] captures the pointer — the lambda then has the same lifetime concerns as the object. [*this] (C++17) copies the whole object into the lambda, so the lambda can outlive the original — at the cost of the copy.
Mutable lambdas
By default, a lambda's operator() is const. Captured-by-value members are also const inside the body, so you cannot mutate them:
auto bad = [n = 0] { ++n; }; // ERROR: n is const inside operator()mutable removes the const:
auto counter = [n = 0]() mutable { return ++n; };
counter(); // 1
counter(); // 2
counter(); // 3This is a stateful lambda — each call advances n. Useful for closures over generators, IDs, sequence numbers.
Init-capture (generalised capture)
C++14 introduced init-capture, also known as generalised capture. Instead of just capturing an existing variable, you can introduce a new lambda-local variable with an initialiser:
auto ptr = std::make_unique<Widget>();
auto consume = [w = std::move(ptr)] { w->run(); };
// ptr is now nullptr; the lambda owns the Widget.This is the only way to move a move-only type (std::unique_ptr, std::thread) into a lambda. Capture-by-value would try to copy and fail to compile.
Init-capture also lets you precompute something at lambda-creation time:
auto check = [denom = compute_denominator()](int x) { return x / denom; };The expression compute_denominator() runs once when the lambda is constructed, not on every call.
Generic lambdas
C++14 made lambdas templates. Use auto (or, since C++20, a template parameter list []<typename T>(T x)) for parameters:
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int + int
add(1.5, 2.5); // double + double
add(std::string("a"), std::string("b")); // string concatThe compiler generates a templated operator() — one instantiation per type combination, just like a function template. The C++20 syntax lets you spell the template parameter for type-relating constraints:
auto same_type = []<typename T>(T a, T b) { return a == b; };
same_type(1, 2); // OK
same_type(1, 2.0); // ERROR: deduction conflict — T cannot be both int and doubleLambdas in STL algorithms — the standard use case
This is what lambdas were designed for:
#include <algorithm>
#include <vector>
#include <print>
struct Person { std::string name; int age; };
int main() {
std::vector<Person> people = {
{"Alice", 30}, {"Bob", 25}, {"Cara", 40}, {"Dan", 28},
};
// Sort by age, ascending.
std::ranges::sort(people, [](const Person &a, const Person &b) {
return a.age < b.age;
});
// Find first person over 30.
auto it = std::ranges::find_if(people, [](const Person &p) {
return p.age > 30;
});
if (it != people.end()) std::println("first over-30: {}", it->name);
// Sum of ages.
int total = 0;
std::ranges::for_each(people, [&total](const Person &p) { total += p.age; });
std::println("total: {}", total);
}Each call site reads as one expression. The comparator is right next to the sort. The accumulator captures total by reference. No named functors, no global functions, no header file growth.
What a lambda actually is
The trick is that there is no magic. A lambda is sugar for an anonymous class with an operator(). The compiler rewrites:
int threshold = 10;
auto over_threshold = [threshold](int x) { return x > threshold; };
bool ok = over_threshold(15);into something like:
class __lambda_xyz {
public:
explicit __lambda_xyz(int t) : threshold_(t) {}
bool operator()(int x) const { return x > threshold_; }
private:
int threshold_;
};
int threshold = 10;
__lambda_xyz over_threshold(threshold);
bool ok = over_threshold(15);Captures become member variables. The body becomes operator(). By-value captures are members of the type captured; by-reference captures are reference members. mutable removes the const from operator(). Init-capture ([v = std::move(x)]) becomes a member initialised from the expression. Generic lambdas ([](auto x){...}) become a templated operator().
Once you internalise this, the rest of the language follows: a lambda is "just" a class instance, so it has a size (sum of captures, often zero), a type, copy/move semantics inherited from the captures, and a deterministic constructor. Empty-capture lambdas have size 1 (no addressable state, but distinct objects still need distinct addresses).
Lambdas as std::function vs template parameter
Two ways to take a lambda as a function argument:
#include <functional>
// Type-erased: any callable matching the signature.
void on_event_fn(std::function<void(int)> handler) {
handler(42);
}
// Templated: the lambda's exact type is deduced.
template <typename F>
void on_event_tmpl(F handler) {
handler(42);
}The std::function version is simpler at the call site and lets you store handlers in containers (std::vector<std::function<void(int)>>), but it has overhead — a small-buffer optimisation in most implementations, falling back to a heap allocation for large captures, plus an indirect call. The templated version inlines the lambda body into the caller, has zero runtime overhead, but generates a new instantiation per lambda type and cannot be stored in a homogeneous container.
Rule of thumb: for hot-path code, use the templated form. For heterogeneous handler tables, signal/slot systems, or stored callbacks that must outlive the calling scope, use std::function. On embedded targets std::function may heap-allocate — see the C++ on Cortex-M reference for fixed-size alternatives.
A complete worked example
Sort and event handler — the two canonical uses — in one file. Build with g++ -std=c++23 -O2 -Wall lambdas.cpp:
#include <algorithm>
#include <functional>
#include <print>
#include <string>
#include <vector>
struct Event { std::string topic; int value; };
class EventBus {
public:
using Handler = std::function<void(const Event &)>;
void subscribe(Handler h) { handlers_.push_back(std::move(h)); }
void publish(const Event &e) const {
for (const auto &h : handlers_) h(e);
}
private:
std::vector<Handler> handlers_;
};
int main() {
// --- 1. Lambda as std::sort comparator.
std::vector<int> nums = {5, 2, 9, 1, 7, 3};
std::ranges::sort(nums, [](int a, int b) { return a > b; }); // descending
for (int n : nums) std::print("{} ", n);
std::println("");
// --- 2. Lambda as event handler callback.
EventBus bus;
int hit_count = 0;
bus.subscribe([&hit_count](const Event &e) {
if (e.topic == "alarm") ++hit_count;
});
bus.subscribe([prefix = std::string("LOG: ")](const Event &e) {
std::println("{}{}={}", prefix, e.topic, e.value);
});
bus.publish({"alarm", 1});
bus.publish({"info", 42});
bus.publish({"alarm", 5});
std::println("alarms: {}", hit_count);
// --- 3. Mutable lambda as a stateful generator.
auto next_id = [n = 0]() mutable { return ++n; };
std::println("ids: {} {} {}", next_id(), next_id(), next_id());
// --- 4. Non-capturing lambda decays to function pointer.
void (*cb)(int) = [](int x) { std::println("cb called with {}", x); };
cb(99);
}Notice the four idioms: comparator, captured-state handler, init-capture handler with a prefix string, mutable stateful generator, and function-pointer decay. Every line is one of those four patterns.
What you learned
- Lambda syntax is
[capture](params) -> ret { body }; most parts are optional. - Captures:
[x]by value,[&x]by reference,[=]/[&]defaults;[this]captures the object pointer. mutablemakes captured-by-value members modifiable inside the body.- Init-capture (
[v = std::move(x)]) is the only way to move move-only types in. - Generic lambdas (
[](auto x){...}) generate a templatedoperator(). - A lambda is just an anonymous class with an
operator(). Captures become members. - Non-capturing lambdas decay to plain function pointers; capturing ones do not.
Exercises
Exercise 10.1. Write a lambda that takes a std::vector<int> by reference and returns the count of even numbers using std::count_if.
Exercise 10.2. Why does this fail to compile, and what's the fix?
auto make_logger() {
std::string prefix = "[INFO] ";
return [&prefix](const std::string &msg) {
std::println("{}{}", prefix, msg);
};
}Exercise 10.3. Convert this hand-written functor to an equivalent lambda:
struct GreaterThan {
int threshold;
explicit GreaterThan(int t) : threshold(t) {}
bool operator()(int x) const { return x > threshold; }
};Next
| Previous | Next |
|---|---|
| C++ Lesson 9 — Move semantics | C++ Lesson 11 — Error handling |
See also: C++ on Cortex-M, C++ tutorial index, Bare-metal C tutorial.