C++ · lesson 15 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7Lambdas and Functional Programming in C++
C++ lambdas run for real: closure sizes, capture timing, mutable, a dangling capture caught by ASan, generic lambdas, std::function, recursion, [*this].
By SkillByExample · Updated
A lambda is an unnamed function object written inline. The compiler turns each one into a small class with an operator(), which is why lambdas make STL algorithms and callbacks short without costing anything at runtime.
Lambda Syntax
The form is [captures](parameters) -> return_type { body }; everything except the brackets and body is optional.
#include <iostream>
#include <string>
int main() {
auto add = [](int a, int b) -> int { return a + b; }; // explicit return type
auto square = [](double x) { return x * x; }; // deduced: double
std::cout << add(3, 4) << " " << square(1.5) << "\n";
int n = 42;
std::string s = "hello";
auto none = [] {};
auto one = [n] { return n; };
auto two = [n, s] { return s.size() + n; };
auto byref = [&n, &s] { return s.size() + n; };
std::cout << "sizeof: none " << sizeof(none) << ", [n] " << sizeof(one)
<< ", [n, s] " << sizeof(two) << ", [&n, &s] " << sizeof(byref) << "\n";
auto other = [] {};
std::cout << std::boolalpha << "same type as an identical lambda? "
<< std::is_same_v<decltype(none), decltype(other)> << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o syntax syntax.cpp
$ ./syntax
7 2.25
sizeof: none 1, [n] 4, [n, s] 40, [&n, &s] 16
same type as an identical lambda? false
The return type of square was deduced as double. The sizeof line shows what a lambda is: an object whose data members are its captures. With no captures it’s an empty class (size 1); capturing an int makes it 4 bytes; adding a std::string copy adds 32 more (40 with padding); capturing both by reference stores two pointers (16). And every lambda expression has its own unique type — two textually identical lambdas are different types, which is why you store them in auto.
Capture Modes
The capture list decides which locals the body can use, and whether it gets a copy or a reference.
#include <iostream>
#include <string>
int main() {
int x = 10;
std::string name = "Alice";
auto by_value = [x, name] { std::cout << "by value: " << x << " " << name << "\n"; };
auto by_ref = [&x, &name] { std::cout << "by ref: " << x << " " << name << "\n"; };
auto mixed = [=, &x] { x = 99; return name.size(); }; // all by value, except x
x = 20; // change the originals after creating the lambdas
name = "Bob";
by_value(); // copies were taken at creation: 10 Alice
by_ref(); // sees the current values: 20 Bob
auto len = mixed();
std::cout << "mixed wrote x = " << x << ", its name copy has " << len << " chars\n";
int counter = 0;
auto increment = [counter]() mutable { return ++counter; }; // modifies its own copy
std::cout << increment() << " " << increment() << " " << increment()
<< ", original counter " << counter << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o captures captures.cpp
$ ./captures
by value: 10 Alice
by ref: 20 Bob
mixed wrote x = 99, its name copy has 5 chars
1 2 3, original counter 0
The key detail is when a value capture happens: at the lambda’s creation. by_value copied 10 and “Alice” before the originals changed, so it still prints them; by_ref reads the variables at call time. [=, &x] captures everything by value except x, so mixed wrote 99 into the real x. mutable lets a lambda modify its own copy — increment counts 1, 2, 3 inside the closure while the outer counter stays 0.
Without mutable, a value capture is read-only:
int main() {
int x = 10;
auto f = [x] { x = 20; }; // value captures are const unless mutable
f();
}
$ g++ -std=c++20 -Wall -Wextra -o const_capture const_capture.cpp 2>&1 | grep error
const_capture.cpp:3:22: error: assignment of read-only variable 'x'
Dangling References
A reference capture is only valid while the referenced variable lives. Returning such a lambda from a function breaks that — AddressSanitizer shows it:
#include <functional>
#include <iostream>
std::function<int()> make_counter() {
int count = 0;
return [&count]() { return ++count; }; // captures a local by reference
}
int main() {
auto next = make_counter(); // count is already gone
std::cout << next() << "\n";
}
$ g++ -std=c++20 -g -fsanitize=address -o dangling dangling.cpp
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./dangling 2>&1 | grep -E 'ERROR|READ|#0 .* in operator'
==5493==ERROR: AddressSanitizer: stack-use-after-return on address 0x7fda032000b0 at pc 0x55e3280703b5 bp 0x7fff9b114180 sp 0x7fff9b114170
READ of size 4 at 0x7fda032000b0 thread T0
#0 0x55e3280703b4 in operator() /tmp/demo/dangling.cpp:5
count lived in make_counter’s stack frame, which is gone by the time next() runs, so the lambda reads freed stack memory — stack-use-after-return, in operator() at line 5. Without the sanitizer this build crashed with a segmentation fault on this machine, but undefined behavior can just as easily print a plausible number. Capture by value ([count]() mutable) whenever a lambda may outlive its scope.
Lambdas with STL Algorithms
Lambdas are at their best as the predicate or operation of an algorithm, right at the call site.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <numeric>
#include <vector>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9, 3, 7};
int threshold = 4;
std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; });
std::vector<int> big;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(big),
[threshold](int n) { return n > threshold; }); // uses a capture
auto small = std::count_if(nums.begin(), nums.end(),
[threshold](int n) { return n <= threshold; });
int product = std::accumulate(big.begin(), big.end(), 1,
[](int acc, int n) { return acc * n; });
for (int n : nums) std::cout << n << " ";
std::cout << "\nbigger than " << threshold << ": ";
for (int n : big) std::cout << n << " ";
std::cout << "\nsmall count " << small << ", product of big " << product << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o algorithms algorithms.cpp
$ ./algorithms
9 8 7 5 3 2 1
bigger than 4: 9 8 7 5
small count 3, product of big 2520
The comparator a > b sorts descending. copy_if and count_if both use the captured threshold, which would have required a hand-written functor class with a constructor before C++11. accumulate’s lambda takes the running value first and the element second: 9 × 8 × 7 × 5 = 2520.
Generic Lambdas
auto parameters (C++14) make operator() a template; C++20 adds explicit template parameters and requires clauses.
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>
int main() {
auto add = [](auto a, auto b) { return a + b; }; // C++14: a template operator()
std::cout << add(1, 2) << " " << add(1.5, 2.25) << " "
<< add(std::string("hi"), "!") << "\n";
auto first = []<typename T>(const std::vector<T>& v) { return v.front(); }; // C++20
std::cout << first(std::vector<int>{7, 8}) << " "
<< first(std::vector<std::string>{"x", "y"}) << "\n";
auto positive = [](auto val) requires std::is_arithmetic_v<decltype(val)> {
return val > 0;
};
std::cout << std::boolalpha << positive(3) << " " << positive(-2.5) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o generic generic.cpp
$ ./generic
3 3.75 hi!
7 x
true false
One add handled int, double, and std::string + const char*. []<typename T>(const std::vector<T>& v) names T, which auto can’t do when you need the element type. The requires clause restricts positive to arithmetic types — positive(std::string("x")) would fail to compile with “constraints not satisfied”.
Immediately Invoked Lambdas
Defining and calling a lambda in one expression lets you initialize a const variable with multi-step logic.
#include <cstdlib>
#include <iostream>
int main() {
const int level = [] { // complex init for a const variable
const char* env = std::getenv("APP_LEVEL");
if (!env) return 0;
int val = std::atoi(env);
return (val >= 0 && val <= 10) ? val : 0;
}(); // () calls it immediately
std::cout << "level = " << level << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o iife iife.cpp
$ ./iife
level = 0
$ APP_LEVEL=7 ./iife
level = 7
$ APP_LEVEL=42 ./iife
level = 0
The trailing () runs the lambda once, and level is const from birth — no mutable temporary, no separate helper function. Missing and out-of-range values both fall back to 0.
std::function — Type-Erased Callables
std::function<R(Args...)> holds any callable with a matching signature, so different lambdas and functions can share one container or member type.
#include <functional>
#include <iostream>
#include <string>
#include <vector>
struct Button {
std::string label;
std::function<void()> on_click; // any callable with this signature
void click() const { if (on_click) on_click(); else std::cout << label << ": no handler\n"; }
};
int triple(int x) { return x * 3; }
int main() {
std::vector<std::function<int(int)>> steps;
steps.push_back([](int x) { return x * 2; });
steps.push_back(triple); // a plain function fits too
int offset = 10;
steps.push_back([offset](int x) { return x + offset; });
int val = 3;
for (const auto& f : steps) val = f(val);
std::cout << "((3*2)*3)+10 = " << val << "\n";
Button ok{"OK", [] { std::cout << "OK clicked\n"; }}, cancel{"Cancel", nullptr};
ok.click();
cancel.click();
std::cout << "sizeof(std::function<int(int)>) = " << sizeof(std::function<int(int)>) << "\n";
try { cancel.on_click(); } catch (const std::bad_function_call& e) { std::cout << e.what() << "\n"; }
}
$ g++ -std=c++20 -Wall -Wextra -o function function.cpp
$ ./function
((3*2)*3)+10 = 28
OK clicked
Cancel: no handler
sizeof(std::function<int(int)>) = 32
bad_function_call
The vector mixes a captureless lambda, a plain function and a capturing lambda: 3 → 6 → 18 → 28. An empty std::function tests false, so click() can check it; calling one anyway throws std::bad_function_call. The flexibility has a cost: every std::function is 32 bytes here regardless of what it holds, larger closures go on the heap, and calls go through an indirection the optimizer usually can’t see through. When the callable’s type can be a template parameter or auto, prefer that.
Recursive Lambdas
A lambda can’t name itself inside its own body. Before C++23 there are two workarounds:
#include <functional>
#include <iostream>
int main() {
std::function<int(int)> fib = [&fib](int n) { // captures itself by reference
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
};
auto fact = [](auto& self, int n) -> long { // pass itself as a parameter
return n <= 1 ? 1 : n * self(self, n - 1);
};
std::cout << fib(10) << " " << fact(fact, 10) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o recursive recursive.cpp
$ ./recursive
55 3628800
fib captures the std::function that holds it — it works, with std::function’s overhead. fact takes itself as a parameter, which needs no std::function but makes every call site write self(self, …). C++23’s explicit object parameter (“deducing this”) removes the awkwardness:
#include <iostream>
int main() {
auto fib = [](this auto& self, int n) -> int { // C++23: deducing this
return n <= 1 ? n : self(n - 1) + self(n - 2);
};
std::cout << fib(10) << "\n";
}
$ clang++ -std=c++23 -Wall -Wextra -o deducing_this deducing_this.cpp
$ ./deducing_this
55
This needs Clang 18 or GCC 14; GCC 13, the other compiler on this machine, rejects this auto& self with “expected identifier before ‘this’”.
Capturing this
Inside a member function, [this] captures the object’s pointer; C++17’s [*this] copies the object.
#include <functional>
#include <iostream>
class Processor {
int threshold_ = 5;
public:
std::function<bool(int)> live() { return [this](int x) { return x > threshold_; }; }
std::function<bool(int)> snapshot() { return [*this](int x) { return x > threshold_; }; }
void set_threshold(int t) { threshold_ = t; }
};
int main() {
Processor p;
auto by_pointer = p.live(); // holds &p
auto by_copy = p.snapshot(); // holds a copy of p, taken now
p.set_threshold(100);
std::cout << std::boolalpha << "7 > threshold? live: " << by_pointer(7)
<< ", snapshot: " << by_copy(7) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o capture_this capture_this.cpp
$ ./capture_this
7 > threshold? live: false, snapshot: true
After set_threshold(100), the [this] lambda sees the new threshold and says 7 is not above it; the [*this] lambda carries its own copy of the object from before the change and still uses 5. That copy is also what keeps it safe if p is destroyed before the callback runs — [this] would dangle, like the reference capture above.
Stateless Lambdas and Function Pointers
A lambda with no captures converts to a plain function pointer, so it works with C APIs.
#include <cstdio>
#include <cstdlib>
int main() {
int data[] = {42, 7, 19, 3};
// C's qsort takes a plain function pointer: int (*)(const void*, const void*)
std::qsort(data, 4, sizeof(int), [](const void* a, const void* b) {
int x = *static_cast<const int*>(a), y = *static_cast<const int*>(b);
return (x > y) - (x < y);
});
for (int x : data) std::printf("%d ", x);
std::printf("\n");
int (*fp)(int) = [](int x) { return x * 2; }; // explicit conversion target
std::printf("fp(21) = %d\n", fp(21));
}
$ g++ -std=c++20 -Wall -Wextra -o fnptr fnptr.cpp
$ ./fnptr
3 7 19 42
fp(21) = 42
qsort wants an int (*)(const void*, const void*), and the captureless lambda converts to exactly that. (x > y) - (x < y) returns -1, 0 or 1 without the overflow that x - y risks. A capture gives the lambda state that a bare function pointer has nowhere to keep:
int main() {
int factor = 3;
int (*fp)(int) = [factor](int x) { return x * factor; }; // has state
return fp(1);
}
$ g++ -std=c++20 -Wall -Wextra -o fnptr_fail fnptr_fail.cpp 2>&1 | grep error
fnptr_fail.cpp:3:22: error: cannot convert 'main()::<lambda(int)>' to 'int (*)(int)' in initialization
Practice
-
Rewrite
make_counterindangling.cppso it’s safe, and callnext()three times.Solution
std::function<int()> make_counter() { int count = 0; return [count]() mutable { return ++count; }; // the closure owns its copy }Prints 1, 2, 3, and ASan stays silent. The counter now lives inside the closure object, which lives as long as
next. -
In
captures.cpp, changeby_valueto[=]and predict the output before running.Solution
Still
by value: 10 Alice.[=]captures exactly the variables the body uses, by value, at creation time — the same as listing[x, name]. -
Use a generic lambda with
std::sortto sort astd::vector<std::string>by length, shortest first, ties alphabetical.Solution
std::sort(words.begin(), words.end(), [](const auto& a, const auto& b) { return a.size() != b.size() ? a.size() < b.size() : a < b; });{"pear", "fig", "apple", "kiwi"}becomesfig kiwi pear apple. -
Store three lambdas with different capture lists in one
std::vectorwithoutstd::function. Why doesn’t it work?Solution
It doesn’t compile: each lambda has its own unique type, and a vector holds one type.
std::vector<decltype(first)>can only ever hold copies of that first lambda. Heterogeneous callables need type erasure —std::function(orstd::move_only_functionin C++23). -
Make
fnptr_fail.cppcompile while still multiplying by a runtimefactor.Solution
int factor = 3; std::function<int(int)> f = [factor](int x) { return x * factor; }; return f(1);Or use
auto f = .... A raw function pointer can’t carryfactor; for a C API, the usual workaround is itsvoid* user_dataparameter.