Lambda expressions
A lambda is a function written where it is passed. The compiler turns it into an unnamed class with an operator(), so it is a function object with a short syntax, and the algorithms and containers of the previous lessons accept it without a named function.
The first decision is what the lambda captures: nothing, a copy of a local, or a reference to it. That choice decides whether the lambda can outlive the scope, whether it can be copied, and whether it can be stored in a std::function or in a plain function pointer.
A function without a name
[](int value) { return value * 2; } declares a lambda and auto twice = ... stores it. The variable is an object with a call operator, so twice(4) calls it like a function.
The program defines a lambda that adds a constant, calls it twice and passes it to std::transform. The lambda has no captures, so it is a plain function object with no state.
The compiler names the type for you, which is why auto is the only practical way to store a lambda. The type has no name a programmer can write, and it differs for every lambda in the program.
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
auto add_ten = [](int value) { return value + 10; };
std::cout << add_ten(1) << ' ' << add_ten(2) << '\n';
std::vector<int> values{1, 2, 3};
std::transform(values.begin(), values.end(), values.begin(), add_ten);
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra first.cpp -o first
./first
11 12
11 12 13
Capturing a local
A lambda that mentions a local variable has to capture it. [x] captures a copy, [&x] captures a reference, and [=] or [&] capture everything the body uses.
The program captures a counter by value and by reference. The by-value lambda gets a copy at the moment it is created, so later changes to the local are invisible; the by-reference lambda sees the live variable.
Capture by reference is cheap and dangerous: the lambda must not outlive the scope. Capture by value is safe to copy and store, and it is the right default for a lambda that is kept.
#include <iostream>
int main()
{
int counter = 0;
int copy = 0;
auto by_reference = [&counter]() { ++counter; };
auto by_value = [copy]() { return copy + 100; };
for (int i = 0; i < 3; ++i) {
by_reference();
}
std::cout << counter << ' ' << by_value() << '\n';
}
g++ -std=c++23 -Wall -Wextra capture.cpp -o capture
./capture
3 100
A mutable lambda keeps state
A lambda that captures by value gets its own copy, and that copy is const inside the call operator. Adding mutable removes the const, so the lambda can change its own state between calls.
The program makes a counter lambda that increments its copy on every call. The three calls print 1, 2 and 3, and the local it captured is untouched.
This is a function object with state, written in one line. The same thing as a named class is the subject of lesson 053, and the two are interchangeable wherever a callable is expected.
#include <iostream>
int main()
{
int start = 0;
auto counter = [start]() mutable { return ++start; };
std::cout << counter() << ' ' << counter() << ' ' << counter() << '\n';
std::cout << start << '\n';
}
g++ -std=c++23 -Wall -Wextra mutable.cpp -o mutable_lambda
./mutable_lambda
1 2 3
0
Generic lambdas and init captures
A lambda parameter declared auto makes the call operator a template, so one lambda prints an int, a double and a string. This is a generic lambda, and it is the shortest way to write a function template with no other purpose.
An init capture declares a new variable in the capture list: [name = std::string{"x"}] copies or moves a value into the lambda's state under a new name. The program moves a string into the lambda and prints it, which is how a lambda takes ownership of something.
The two features meet in the transform call: a generic lambda multiplies each element by the factor it captured. The element type comes from the container and the factor from the capture.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
auto show = [](const auto& value) { std::cout << value << ' '; };
show(1);
show(2.5);
show(std::string{"three"});
std::cout << '\n';
std::vector<int> values{1, 2, 3};
int factor = 10;
auto scale = [factor = factor](auto value) { return value * factor; };
std::transform(values.begin(), values.end(), values.begin(), scale);
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra generic.cpp -o generic
./generic
1 2.5 three
10 20 30
A capture cannot become a function pointer
A lambda with no captures converts to a plain function pointer, because it is just a function call. A lambda with a capture carries state, so it has no such conversion and the compiler rejects the assignment.
The program stores a capturing lambda in a void (*)(int) and the error says the conversion is not available and names the lambda's type. The fix is std::function from the next lesson, or a lambda without captures.
The distinction matters when a C API takes a callback: a function pointer has nowhere to keep the captured values, which is why such APIs also take a void* user data parameter.
#include <iostream>
int main()
{
int counter = 0;
void (*callback)(int) = [&counter](int value) { counter += value; };
callback(3);
std::cout << counter << '\n';
}
g++ -std=c++23 -Wall -Wextra function_pointer.cpp -o function_pointer
function_pointer.cpp: In function 'int main()':
function_pointer.cpp:6:29: error: cannot convert 'main()::<lambda(int)>' to 'void (*)(int)' in initialization
6 | void (*callback)(int) = [&counter](int value) { counter += value; };
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| |
| main()::<lambda(int)>
Exercises
- Sort a vector of strings with a lambda that compares lengths, and then with one that compares the last character.
- Write a lambda that captures a vector by reference and appends a value, and call it three times.
- Move a std::string into a lambda with an init capture and print it from inside.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the compiler turn a lambda into?
Answer
An unnamed class with an operator()
The closure type holds the captures and provides the call operator.
-
What does
[&]capture?Answer
Every local used by the lambda, by reference
The default capture by reference shares the variables with the enclosing scope.
-
What does a
mutablelambda allow?Answer
The call operator to change the captured copies
Without mutable the call operator is const, so the captured copies are read-only.
-
What is an init capture?
Answer
A new variable created in the capture list
[value = compute()]creates the variable inside the closure. -
What does this program print?
#include <iostream> #include <vector> int main() { auto add = [](int a, int b) { return a + b; }; std::cout << add(5, 6) << ' ' << add(7, 5) << '\n'; std::vector<int> values{1, 2}; int extra = 10; auto withExtra = [extra](int v) { return v + extra; }; for (int v : values) std::cout << withExtra(v) << ' '; }Answer
11 12 / 11 12
The first lambda adds its two arguments, and the second adds the captured copy of 10.
-
What does this program print?
#include <iostream> int main() { int value = 3; int copy = 100; auto byRef = [&value] { value = 42; }; auto byValue = [copy]() mutable { copy = 7; }; byRef(); byValue(); std::cout << value << ' ' << copy; }Answer
42 100
The reference capture changes the original, and the mutable capture changes only the closure's copy.
-
What does this program print?
#include <iostream> int main() { int count = 0; auto next = [count]() mutable { return ++count; }; std::cout << next() << ' ' << next() << ' ' << next() << '\n'; std::cout << count; }Answer
1 2 3 / 0
The mutable copy keeps its state between calls, and the original stays zero.
-
What does this program print?
#include <iostream> #include <vector> int main() { auto show = [](const auto& value) { std::cout << value << ' '; }; show(1); show(2.5); show("three"); std::cout << '\n'; std::vector<int> values{10, 20, 30}; int factor = 2; for (int v : values) std::cout << [factor](int x) { return x * factor; }(v) << ' '; }Answer
1 2.5 three / 20 40 60
The generic lambda prints each type, and the init capture multiplies each element by two.
-
What does the compiler say about this line?
int main() { int counter = 0; void (*callback)(int) = [&counter](int value) { counter += value; }; callback(1); }Answer
cannot convert 'main()::<lambda(int)>' to 'void (*)(int)'
A lambda with captures has state and cannot become a function pointer.
-
When is a capture by value the right choice?
Answer
When the lambda may outlive the variable it reads
The copy inside the closure keeps the value alive after the original scope ends.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev