Last Update:
Lambdas: From C++11 to C++20, Part 1
Table of Contents
Lambda expressions are one of the most powerful additions to C++11, and they continue to evolve with each new C++ language standard. In this article, we’ll go through history and see the evolution of this crucial part of modern C++.
The second part is available:
Lambdas: From C++11 to C++20, Part
2
Intro
At one of our local C++ User Group meeting, we had a live coding session about the “history” of lambda expressions. The talk was lead by a C++ Expert Tomasz Kamiński (see Tomek’s profile at Linkedin). See this event:
Lambdas: From C++11 to C++20 - C++ User Group Krakow
I’ve decided to take the code from Tomek (with his permission!), describe it and form a separate article.
We’ll start by learning about C++03 and the need of having compact, local functional expressions. Then we’ll move on to C++11 and C++14. In the second part of the series, we’ll see changes from C++17, and we’ll even have a peek of what will happen in C++20.
“Lambdas” in C++03
Since the early days of STL, std::algorithms
- like std::sort
could
take any callable object and call it on elements of the container.
However, in C++03 it meant only function pointers and functors.
For example:
#include <iostream>
#include <algorithm>
#include <vector>
struct PrintFunctor {
void operator()(int x) const {
std::cout << x << std::endl;
}
};
int main() {
std::vector<int> v;
v.push_back(1);
v.push_back(2);
std::for_each(v.begin(), v.end(), PrintFunctor());
}
Runnable code: @Wandbox
But the problem was that you had to write a separate function or a functor in a different scope than the invocation of the algorithm.
As a potential solution, you could think about writing a local functor class - since C++ always has support for that syntax. But that didn’t work…
See this code:
int main() {
struct PrintFunctor {
void operator()(int x) const {
std::cout << x << std::endl;
}
};
std::vector<int> v;
v.push_back(1);
v.push_back(2);
std::for_each(v.begin(), v.end(), PrintFunctor());
}
Try to compile it with -std=c++98
and you’ll see the following error
on GCC:
error: template argument for
'template<class _IIter, class _Funct> _Funct
std::for_each(_IIter, _IIter, _Funct)'
uses local type 'main()::PrintFunctor'
Basically, in C++98/03 you couldn’t instantiate a template with a local type.
Because all of those limitations the Committee started to design a new feature, something that we can create and call “in place”… “lambda expressions”!
If we look at N3337 - the final draft of C++11, we can see a separate section for lambdas: [expr.prim.lambda].
Moving to C++11
Lambdas were added into the language in a smart way I think. They use some new syntax, but then the compiler “expands” it into a real class. This way we have all advantages (and disadvantages sometimes) of the real strongly typed language.
Here’s a basic code example that also shows the corresponding local functor object:
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
struct {
void operator()(int x) const {
std::cout << x << '\n';
}
} someInstance;
std::vector<int> v;
v.push_back(1);
v.push_back(2);
std::for_each(v.begin(), v.end(), someInstance);
std::for_each(v.begin(), v.end(), [] (int x) {
std::cout << x << '\n';
}
);
}
Live example @WandBox
You can also check out CppInsights that shows how the compiler expands the code:
See this sample:
CppInsighs: lambda
test
In the example the compiler transforms:
[] (int x) { std::cout << x << '\n'; }
Into something like that (simplified form):
struct {
void operator()(int x) const {
std::cout << x << '\n';
}
} someInstance;
The syntax of the lambda expression:
[] () { code; }
^ ^ ^
| | |
| | optional: mutable, exception, trailing return, ...
| |
| parameter list
|
lambda introducer with capture list
Some definitions before we start:
From [expr.prim.lambda#2]:
The evaluation of a lambda-expression results in a prvalue temporary . This temporary is called the closure object.
And from [expr.prim.lambda#3]:
The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type — called the closure type.
A few examples of lambda expressions:
For example:
[](float f, int a) { return a*f; }
[](MyClass t) -> int { auto a = t.compute(); return a; }
[](int a, int b) { return a < b; }
The Type of a Lambda
Since the compiler generates some unique name for each lambda, there’s no way to know it upfront.
That’s why you have to use auto
(or decltype
)) to deduce the type.
auto myLambda = [](int a) -> double { return 2.0 * a; }
What’s more [expr.prim.lambda]:
The closure type associated with a lambda-expression has a deleted ([dcl.fct.def.delete]) default constructor and a deleted copy assignment operator.
That’s why you cannot write:
auto foo = [&x, &y]() { ++x; ++y; };
decltype(foo) fooCopy;
This gives the following error on GCC:
error: use of deleted function 'main()::<lambda()>::<lambda>()'
decltype(foo) fooCopy;
^~~~~~~
note: a lambda closure type has a deleted default constructor
The Call Operator
The code that you put into the lambda body is “translated” to the code
in the operator()
of the corresponding closure type.
By default it’s a const inline method. You can change it by specifying
mutable
after the parameter declaration clause:
auto myLambda = [](int a) mutable { std::cout << a; }
While a const method is not an “issue” for a lambda without an empty capture list… it makes a difference when you want to capture.
Captures
The []
does not only introduce the lambda but also holds a list of
captured variables. It’s called “capture clause”.
By capturing a variable, you create a member copy of that variable in the closure type. Then, inside the lambda body, you can access it.
The basic syntax:
[&]
- capture by reference, all automatic storage duration variable declared in the reaching scope[=]
- capture by value, a value is copied[x, &y]
- capturex
by value andy
by a reference explicitly
For example:
int x = 1, y = 1;
{
std::cout << x << " " << y << std::endl;
auto foo = [&x, &y]() { ++x; ++y; };
foo();
std::cout << x << " " << y << std::endl;
}
You can play with the full example @Wandbox
While specifying [=]
or [&]
might be handy - as it captures all
automatic storage duration variable, it’s clearer to capture a variable
explicitly. That way the compiler can warn you about unwanted effects
(see notes about global and static variable for example)
You can also read more in item 31 in “Effective Modern C++” by Scott Meyers: “Avoid default capture modes.”
And an important quote:
The C++ closures do not extend the lifetimes of the captured references.
Mutable
By default operator()
of the closure type is const, and you cannot
modify captured variables inside the body of the lambda.
If you want to change this behaviour you need to add mutable
keyword
after the parameter list:
int x = 1, y = 1;
std::cout << x << " " << y << std::endl;
auto foo = [x, y]() mutable { ++x; ++y; };
foo();
std::cout << x << " " << y << std::endl;
In the above example, we can change the values of x
and y
… but those
are only copies of x
and y
from the enclosing scope.
Capturing Globals
If you have a global value and then you use [=]
in your lambda you
might think that also a global is captured by value… but it’s not.
int global = 10;
int main()
{
std::cout << global << std::endl;
auto foo = [=] () mutable { ++global; };
foo();
std::cout << global << std::endl;
[] { ++global; } ();
std::cout << global << std::endl;
[global] { ++global; } ();
}
Play with code @Wandbox
Only variables with automatic storage duration are captured. GCC can even report the following warning:
warning: capture of variable 'global' with non-automatic storage duration
This warning will appear only if you explicitly capture a global
variable, so if you use [=]
the compiler won’t help you.
The Clang compiler is even more helpful, as it generates an error:
error: 'global' cannot be captured because it does not have automatic storage duration
See @Wandbox
Capturing Statics
Similarly to capturing a global variable, you’ll get the same with a static variable:
#include <iostream>
void bar()
{
static int static_int = 10;
std::cout << static_int << std::endl;
auto foo = [=] () mutable { ++static_int; };
foo();
std::cout << static_int << std::endl;
[] { ++static_int; } ();
std::cout << static_int << std::endl;
[static_int] { ++static_int; } ();
}
int main()
{
bar();
}
Play with code @Wandbox
The output:
10
11
12
And again, this warning will appear only if you explicitly capture a
global variable, so if you use [=]
the compiler won’t help you.
Capturing a Class Member
Do you know what will happen with the following code:
#include <iostream>
#include <functional>
struct Baz
{
std::function<void()> foo()
{
return [=] { std::cout << s << std::endl; };
}
std::string s;
};
int main()
{
auto f1 = Baz{"ala"}.foo();
auto f2 = Baz{"ula"}.foo();
f1();
f2();
}
The code declares a Baz
object and then invokes foo()
. Please note
that foo()
returns a lambda (stored in std::function
) that captures
a member of the class.
Since we use temporary objects, we cannot be sure what will happen when
you call f1
and f2
. This is a dangling reference problem and
generates Undefined Behaviour.
Similarly to:
struct Bar {
std::string const& foo() const { return s; };
std::string s;
};
auto&& f1 = Bar{"ala"}.foo(); // dangling reference
Play with code @Wandbox
Again, if you state the capture explicitly ([s]
):
std::function<void()> foo()
{
return [s] { std::cout << s << std::endl; };
}
The compiler will prevent you for making this mistake, by emitting errors:
In member function 'std::function<void()> Baz::foo()':
error: capture of non-variable 'Baz::s'
error: 'this' was not captured for this lambda function
...
See in this example @Wandbox
Move-able-only Objects
If you have an object that is movable only (for example unique_ptr
),
then you cannot move it to lambda as a captured variable. Capturing by
value does not work, so you can only capture by reference… however this
won’t transfer the ownership, and it’s probably not what you wanted.
std::unique_ptr<int> p(new int{10});
auto foo = [p] () {}; // does not compile....
Preserving Const
If you capture a const variable, then the constness is preserved:
int const x = 10;
auto foo = [x] () mutable {
std::cout << std::is_const<decltype(x)>::value << std::endl;
x = 11;
};
foo();
Test code @Wandbox
Return Type
In C++11 you could skip the trailing return type of the lambda and then the compiler would deduce the type for you.
Initially, return type deduction was restricted to lambdas with bodies containing a single return statement, but this restriction was quickly lifted as there were no issues with implementing a more convenient version.
See C++ Standard Core Language Defect Reports and Accepted Issues (thanks Tomek for finding the correct link!)
So since C++11, the compiler could deduce the return type as long as all of your return statements are convertible to the same type.
If all return statements return an expression and the types of the returned expressions after lvalue-to-rvalue conversion (7.1 [conv.lval]), array-to-pointer conversion (7.2 [conv.array]), and function-to-pointer conversion (7.3 [conv.func]) are the same, that common type;
auto baz = [] () {
int x = 10;
if ( x < 20)
return x * 1.1;
else
return x * 2.1;
};
Play with the code @Wandbox
In the above lambda, we have two returns statements, but they all point
to double
so the compiler can deduce the type.
IIFE - Immediately Invoked Function Expression
In our examples I defined a lambda and then invoked it by using a closure object… but you can also invoke it immediately:
int x = 1, y = 1;
[&]() { ++x; ++y; }(); // <-- call ()
std::cout << x << " " << y << std::endl;
Such expression might be useful when you have a complex initialisation of a const object.
const auto val = []() { /* several lines of code... */ }();
I wrote more about it in the following blog post: IIFE for Complex Initialization.
Conversion to function pointer
The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.
In other words, you can convert a lambda without captures to a function pointer.
For example
#include <iostream>
void callWith10(void(* bar)(int))
{
bar(10);
}
int main()
{
struct
{
using f_ptr = void(*)(int);
void operator()(int s) const { return call(s); }
operator f_ptr() const { return &call; }
private:
static void call(int s) { std::cout << s << std::endl; };
} baz;
callWith10(baz);
callWith10([](int x) { std::cout << x << std::endl; });
}
Play with the code @Wandbox
Improvements in C++14
The standard N4140 and lambdas: [expr.prim.lambda].
C++14 added two significant enhancements to lambda expressions:
- Captures with an initialiser
- Generic lambdas
The features can solve several issues that were visible in C++11.
Return Type
Lambda return type deduction was updated to conform to the rules of
auto
deduction rules for functions.
The lambda return type is auto, which is replaced by the trailing-return-type if provided and/or deduced from return statements as described in [dcl.spec.auto].
Captures With an Initializer
In short, we can create a new member variable of the closure type and then use it inside the lambda.
For example:
int main() {
int x = 10;
int y = 11;
auto foo = [z = x+y]() { std::cout << z << '\n'; };
foo();
}
It can solve a few problems, for example with movable only types.
Move
Now, we can move an object into a member of the closure type:
#include <memory>
int main()
{
std::unique_ptr<int> p(new int{10});
auto foo = [x=10] () mutable { ++x; };
auto bar = [ptr=std::move(p)] {};
auto baz = [p=std::move(p)] {};
}
Optimisation
Another idea is to use it as a potential optimisation technique. Rather than computing some value every time we invoke a lambda, we can compute it once in the initialiser:
#include <iostream>
#include <algorithm>
#include <vector>
#include <memory>
#include <iostream>
#include <string>
int main()
{
using namespace std::string_literals;
std::vector<std::string> vs;
std::find_if(vs.begin(), vs.end(), [](std::string const& s) {
return s == "foo"s + "bar"s; });
std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; });
}
Capturing a Member Variable
Initialiser can also be used to capture a member variable. We can then capture a copy of a member variable and don’t bother with dangling references.
For example
struct Baz
{
auto foo()
{
return [s=s] { std::cout << s << std::endl; };
}
std::string s;
};
int main()
{
auto f1 = Baz{"ala"}.foo();
auto f2 = Baz{"ula"}.foo();
f1();
f2();
}
Play with code @Wandbox
In foo()
we capture a member variable by copying it into the closure
type. Additionally, we use auto
for the deduction of the whole method
(previously, in C++11 we could use std::function
).
Generic Lambdas
Another significant improvement to Lambdas is a generic lambda.
Since C++14 you can now write:
auto foo = [](auto x) { std::cout << x << '\n'; };
foo(10);
foo(10.1234);
foo("hello world");
This is equivalent to using a template declaration in the call operator of the closure type:
struct {
template<typename T>
void operator()(T x) const {
std::cout << x << '\n';
}
} someInstance;
Such generic lambda might be very helpful when deducting type is hard.
For example:
std::map<std::string, int> numbers {
{ "one", 1 }, {"two", 2 }, { "three", 3 }
};
// each time entry is copied from pair<const string, int>!
std::for_each(std::begin(numbers), std::end(numbers),
[](const std::pair<std::string, int>& entry) {
std::cout << entry.first << " = " << entry.second << '\n';
}
);
Did I make any mistake here? Does entry
have the correct type?
.
.
.
Probably not, as the value type for std::map
is
std::pair<const Key, T>
. So my code will perform additional string
copies…
This can be fixed by using auto
:
std::for_each(std::begin(numbers), std::end(numbers),
[](auto& entry) {
std::cout << entry.first << " = " << entry.second << '\n';
}
);
You can play with code @Wandbox
Summary
What a story!
In this article, we started from the early days of lambda expression in
C++03 and C++11, and we moved into an improved version in C++14.
You saw how to create a lambda, what’s the basic structure of this
expression, what’s capture clause and many more.
In the next part of the article, we’ll move to C++17, and we’ll also have a glimpse of the future C++20 features.
The second part is available:
Lambdas: From C++11 to C++20, Part
2
Have I skipped something?
Maybe you have some interesting example to share?
Please let me know in comments!
References
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: