:: C++ lambda: The basics
If you have a Math background, you’ll have probably already heard about Lambda. In Programming in general and in C++ specifically, the concept may not resemble at all at what you think but don’t be afraid, in this post I’ll briefly introduce you to what Lambdas are and how to use and take advantage from them.
Before lambda: the earlier age …
The Standard Template Library (STL) is composed of containers. Iterating over them is quite easy, you can choose to use
a build-in Algorithm like for_each
or even use a raw for
loop. But as a rule of thumb it’s recommended to use standard algorithms, because they express well the intend and make the code cleaner.
The for_each
algorithm overload that we will use takes three parameters:
-
The first one is an iterator pointing at the beginning of the container
-
The second one is another iterator but that points at this time after the end of the container
-
The third one is a Callable
A Callable maybe a class or a struct that overrides the bracket operator (operator()
), or a function pointer/reference.
Let’s enlight our words with the following example:
1
2
3
4
5
6
#include <iostream>
struct TestCallable {
void operator() (int param){
std::cout << param << "\n";
}
};
When passed as the third argument of for_each
the operator will be called with the specified argument.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <algorithm>
#include <array>
#include <iostream>
struct TestCallable {
void operator()(int param) { std::cout << param << "\n"; }
};
auto main() -> int {
std::array<int, 4> a = {1, 2, 3, 4};
std::for_each(a.cbegin(), a.cend(), TestCallable());
return 1;
}
The above snippet will print the elements of the container. This is what we call High Order Functions (HOF).
As you can see we were obliged to declare an external class and then overrides its bracket operator before being able to use it as a callback in the for_each
algorithm. It maybe annoying having to do that everytime that we need a callback for doing a specific task.
Wait, you’re saying that everytime that I need a callback I’ll have to write such class? I’m abandoning here …
No don’t run out, I’m sure you have guessed that if I’m talking to you about that “problem” it’s because a solution actually exists, just keep scrolling .
Lambda to the rescue
Lambda is a feature introduce since C++11. It provides a concise way to create simple function object. The code we wrote earlier can be written like this using lambdas :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Compiler Explorer : https://godbolt.org/z/Fr4utK
#include <array>
#include<iostream>
#include <algorithm>
auto main() -> int {
std::array<int,4> a = {1,2,3,4};
std::for_each( a.cbegin(),a.cend(), [](auto param){
std::cout << param << "\n";
});
return 1;
}
Note that we are not anymore using TestCallable
as the third parameter.
The two code will basically perform the same task, just that this one is using a lambda. Cleaner right?
Dude is that obscure syntax you want me to introduce in my code base?
Well, the lambda syntax can be obscure but once you’ve understood the meaning of each branches you will feel that your soul is now different from others
Depicting the different parts of a Lambda
Overall structure
A lambda is a closure type. It’s structured as follow since C++11:
[](parameters) *specifier* -> *trailing return type* { *body* }
-
[]
: called the capture list or lambda introducer. It’s there that we will specify the variables that will be available in the Lambda scope. It helps introducing the lambda. -
(parameters)
: Called the parameter list. It’s inside the brackets that we will specify the parameters that the lambda will take when called. You probably already know this from traditionnal functions. The brackets are optional. -
specifier
: It can be eitherconstexpr
ormutable
, it’s an optional value. -
Trailing return type
: It’s an optoinal value, preceeded by an arrow->
which specifies that we are using the trailing return type syntax. When not specified the return type of the lambda will be deduced using template type deduction rules. -
body
: It’s where you write the content of your lambda.
With all that on hands, we can now write as much examples as we can:
Example One:
1
2
3
4
5
6
7
8
9
10
// Compiler Explorer https://godbolt.org/z/ysp-p_
#include <iostream>
auto main() -> int {
auto sumLambda = [](int a,int b) -> int { return a + b; }; // Declaring about lambda
auto sum = sumLambda(4,5); // Calling the lambda with 4 and 5 as parameters
std::cout<< "The sum is: "<< sum << "\n";
return 1;
}
Example Two:
1
2
3
4
5
6
7
8
9
10
#include <iostream>
auto main() -> int {
int age{20};
auto changeAge = [](){ age = 10; std::cout << age << "\n"; };
changeAge(); // Invoking the lambda at this point taking not parameters.
return 1;
}
When writting such syntax, internally the compiler will generate a unique unnamed class that overrides the operator()
just like we did before. The type of the lambda is only known at compile time, we need to use auto
keyword so that the compiler will replace it by a type he generated.
The generated operator()
by the compiler is by default const
if we didn’t explicitly required it not to be by putting mutable
in the specifier
list.
To visualize how the compiler generates that I used CppInsights which helps us see what the compiler sees . Here is the link to the First Example using cppinsights, https://cppinsights.io/s/3feb5a61. There you’ll see and understand more about what I was describing.
Okay cook but dude, your second example is not running what’s going on?
Oh well, even me I don’t know what’s wrong. Just kidding let’s move to the next section to understand what’s wrong.
Lambda captures list
In the lambda body, variables of the parent scope (the scope in which the lambda was declared) are not available. If you want to make them being available you’ll need to specify that in the capture list. We don’t specify them randomly, there are some rules to follow:
-
[=]
: will capture all variables in the parent scope by copy. -
[&]
: will capture all variables in the parent scope by reference. -
[&a,b]
: will capturea
by reference andb
by value -
[&a]
: will capturea
from the parent scope by reference. It’ll be the only variable captured in this case. -
[a]
: will capturea
from the parent scope by copy. It’ll be the only variable captured in this case. -
[&a,=]
: will capturea
by reference and others by copy.
You should note that any capture may appear only once, so you cannot [a,a]
else the compiler will scream of pain. You also cannot do something like [&, &i](){};
because you are already capturing all by reference, so the second term is not necessary. Those are just few examples of misuses of the capture list that will leave your compiler unhappy.
So coming back to our Example 2 we can rewrite it accordinly:
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
auto main() -> int {
int age{20};
// Defining the lambda and capturing `age` by reference
auto changeAge = [&age](){ age = 10; std::cout << age << "\n"; };
changeAge(); // Invoking the lambda at this point taking not parameters.
return 1;
}
If you were to capture age
by value, you’ll have a compilation error, untill you write mutable
in the specifier
part of the lambda, remember, this is because the generated operator()
is by default const
and in the lambda body we are modifying the value of age
. The code will look like this if you want to capture age
by value:
1
2
3
4
5
6
7
8
9
#include <iostream>
auto main() -> int {
int age{20};
// Defining the lambda and capturing `age` by value
auto changeAge = [age]() mutable { age = 10; std::cout << age << "\n"; };
changeAge(); // Invoking the lambda at this point taking no parameters.
return 1;
}
But if we were only reading the variable age
without modifying it, the mutable
here will be useless.
Note that when capturing by value, a copy of captured symbols are created in the lambda scope, so any modification of those symbols will be on their copy. If you want the modification to affect the original symbol you’ll probably need to capture by reference instead.
Default Capture list with initializers
When capturing variable you may decide to give them some default values. An example will be more illustrative:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <utility>
auto main() -> int {
int fuel = 10;
int time = 1000;
auto changeFuelAndPrint = [fuel = 20, <ime = std::as_const(time)]() {
std::cout << "Fuel default value in lambda: " << fuel << "\n";
std::cout << "Read only ltime: " << ltime << "\n";
};
changeFuelAndPrint(); // Will print 20
return 1;
}
The variable fuel
in the lambda scope will be initialized to 20
. The variable ltime
is captured by reference and initialized with a time
, with the std::as_const
utility, the variable ltime
will be read-only in the lambda body. You can play with that by creating multiple examples.
I noticed that you declare lambda first after then calling it in another line, ca we do better?
Yes sure IIFE ( Immediately Invoked Function Expression)
IIFE and Lambda
Lambda expressions are closure objects. They can be immediately invoked by putting ()
right after their declaration.
1
2
3
4
5
6
7
8
9
10
#include <iostream>
auto main() -> int {
[]() {
std::cout << "Hello IIFE"
<< "\n";
}(); // Invoking the lambda immediately, so it'll print "Hello IIFE"
return 1;
}
Compiler Explorer CppInsights Link
If your Lambda was taking parameters, you’ll need to specify them in the brackets.
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
auto main() -> int {
[](int valOne, int valTwo) {
int r = valOne + valTwo;
std::cout << r << "\n";
}(4, 2); // Invoking the lambda immediately with parameters 4 and 2, it'll
// print 6
return 1;
}
The simplest lambda you can write
1
2
3
4
auto main() -> int {
[]{};
return 1;
}
The code above actually compiles, I won’t argue about it you have all the necessary weapons to understand how it’s behaving
Where to use Lambda
There are really a lot of places where you can take advantage of lambdas:
-
You can use them as callbacks
-
They help in complex initialization scenarios
-
You can use them to perform some inner actions without actually interferring with variables in the parent scope
-
They can be used to implement custom deleters for smart pointers. Employing the lambda way without capture incur no size penalty on the resulting smart pointer.
Lambda expressions come in a really handy way, but you just have not to abuse of them, still consider using traditionnal functions when necessary. If you don’t use them well you’ll fall under missed optimization opportunities.
Final thoughts
Ah, Lambda ! We came to our end !
Personnally I like the syntax of lambda syntax, it becomes intuitive after you understand how it works. As the C++ language evolve quickly, some additions were made to lambda such as templated lambda (C++20) it’s uncovered in this post.
Now that you have enough information about this mindblowing subject, I can now throw you in the nature to go and seek deep information on it
References
1 - https://eel.is/c++draft/expr.prim.lambda
2 - https://en.cppreference.com/w/cpp/language/lambda
3 - https://www.bfilipek.com/2019/02/lambdas-story-part1.html