In this lesson, we introduce Lambdas. They provide a more concise syntax for creating functions. Lambdas give us more options for designing our software, as they have two key differences compared to regular functions:
This makes lambdas the default choice for scenarios where we need simple, one-off functions. For example, if we need a function to pass as the parameter of another function, lambdas are often the default choice.
This is because they make our code faster to write, and more readable. The lambda is defined at the same place it is being used and, if the lambda is used only in that place, we don't need to clutter our code with unnecessary identifiers.
A lambda expression looks like this:
[] () {
std::cout << "Hello from Lambda!";
}
By itself, the expression doesn't do anything. However, we can treat it like any other expression - for example, we call a lambda directly using ()
:
[] () {
std::cout << "Hello from Lambda!";
}();
We can store the lambda as a variable, and can then call it through that variable later:
auto MyLambda { [] () {
std::cout << "Hello from Lambda!";
} };
MyLambda();
However, by far the most common use of a lambda is to pass it to another function:
// Function that accepts a function argument
void CallIfEven(int Num, auto FunctionToCall) {
if (Num % 2 == 0) {
FunctionToCall();
}
}
int main() {
// Passing a lambda to that function
CallIfEven(4, []() {
std::cout << "It's even!"
})
}
Here, we’re always using auto
to let the compiler figure out the lambda type. Explicitly declaring the type of a lambda is quite tricky, and generally not something that is done.
We will introduce better ways of specifying function types in the next lesson.
Between the (
and )
of a lambda expression, we can specify parameters in the normal way:
[](int x, int y){
std::cout << "The total is " << x+y;
};
We have access to all the same techniques as functions, including passing by reference and using const
:
[](const int& x, const int& y){
std::cout << "The total is " << x+y;
};
We also pass arguments into those parameters in the usual way, within the (
and )
when we call our lambda:
auto MyLambda { [](int x, int y){
std::cout << "The total is " << x+y;
} };
MyLambda(2, 5);
When a lambda’s parameter list is empty (i.e., it doesn’t accept any arguments), we have the option of removing it entirely, including the (
and )
:
auto SayHello { []{
std::cout << "Hello!";
} };
SayHello();
By default, the compiler infers the return type of the lambda. This is equivalent to having a function with an auto
return type:
// This will return a boolean
[](int Number){
return Number % 2 == 0;
};
We may want to explicitly set the return type of our lambda. This would behave the same as setting the return type of a regular function. For example, return values will be implicitly cast to the return type we specify.
The syntax for specifying the return type of a lambda looks like this:
[](int Number) -> bool {
return Number % 2 == 0;
};
Normally, we are able to access values from the parent scope. However, this does not work with lambdas. The following code will not compile:
int main() {
int Number { 2 };
[]() {
Number++;
}();
std::cout << "Number is " << Number;
}
To give our lambda access to variables within the scope it is defined, we need to “capture” them.
The []
in a lambda expression is where we capture variables from the parent scope we want to use.
int main() {
int x { 1 };
int y { 1 };
[x, y]() {
x++;
y++;
}();
std::cout << "Total is " << x + y;
}
Our code now compiles. However, it logs out 2
, rather than the 4
we might have expected. This is because, just like parameters, captures are passed by value.
So, we’re incrementing copies of x
and y
rather than the original variables. We can capture by reference using the &
 character:
int main() {
int x { 1 };
int y { 1 };
[&x, &y]() {
x++;
y++;
}();
std::cout << "Total is " << x + y;
}
Our code will now log out 4
const
and mutable
with Lambda CapturesWhen capturing const
values, we will not be able to modify them:
int main () {
const int x { 1 };
[x]() {
// Error - x is const
x++;
}
}
This can be quite restrictive when passing by value as in that scenario, what we're trying to modify isn't x
, rather it is a copy of x
.
Marking our lambda as mutable
works around this:
int main () {
const int x { 1 };
[x]() mutable {
x++;
}
}
We are able to create new variables in the capture clause. This is useful mainly for concisely capturing variables from the parent scope, whilst simultaneously performing simple modifications.
A common use case is when capturing references that we want to declare as const
within the lambda. The standard library's utility
header includes the std::as_const
function to help with this:
#include <utility>
int main () {
int x { 1 };
[&x = std::as_const(x)]() {
// Error - x is const in this lambda
x++;
}
}
For simpler use cases, the compiler can help us create our capture list. It knows what variables we’re using in our lamda, so it can automatically capture them for us.
We can ask it to capture everything we use by value, using the =
symbol in our capture group:
int main () {
int x { 1 };
[=]() {
x++;
}
}
To capture everything by reference, we can use &
:
int main () {
int x { 1 };
[&]() {
x++;
}
}
We can mix and match these techniques as needed. If we’re using a default capture, it needs to be first:
// Capture x by reference
// Capture y by const reference
// Capture everything else by value
[=, &x, &y = std::as_const(y)]() {
// ...
}
— Added section to note that empty parameter lists are optional
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.