📜 ⬆️ ⬇️

Lambda: from C ++ 11 to C ++ 20. Part 1

Good afternoon friends. Today we have prepared for you a translation of the first part of the article “Lambdas: From C ++ 11 to C ++ 20” . The publication of this material is timed to the launch of the “Developer C ++” course, which starts tomorrow.

Lambda expressions are one of the most powerful additions in C ++ 11 and continue to evolve with each new language standard. In this article, we will go through their history and look at the evolution of this important part of modern C ++.


')
The second part is available at the link:
Lambdas: From C ++ 11 to C ++ 20, Part 2

Introduction

At one of the local meetings of the C ++ User Group, we had a live programming session on the “history” of lambda expressions. The conversation was conducted by a C ++ expert Tomasz Kamiński ( see Thomas’s LinkedIn profile ). Here is the event:

Lambdas: From C ++ 11 to C ++ 20 - C ++ User Group Krakow

I decided to take the code from Thomas (with his permission!), Describe it and create a separate article.

We begin by learning C ++ 03 and the need for compact local functional expressions. Then we move on to C ++ 11 and C ++ 14. In the second part of the series, we will see changes in C ++ 17 and even take a look at what happens in C ++ 20.

Lambda in C ++ 03

From the very beginning, STL std::algorithms , such as std::sort , could accept any callable object and call it on container elements. However, in C ++ 03, this involved only pointers to functions 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()); } 

Launch code: @Wandbox

But the problem was that you had to write a separate function or functor in a different scope, and not in the scope of the algorithm call.

As a potential solution, you might consider writing a local class of functors - since C ++ always supports this syntax. But it does not work ...

Look at 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 compiling it with -std=c++98 , and you will see the following error in GCC:

 error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor' 

In fact, in C ++ 98/03 you cannot create an instance of a template with a local type.
Due to all these restrictions, the Committee began to develop a new feature, which we can create and call "on the spot" ... "lambda expressions"!

If we look at N3337 - the final version of C ++ 11, we will see a separate section for lambda: [expr.prim.lambda] .

Coming to C ++ 11

I think that lambdas have been added to the language wisely. They use the new syntax, but then the compiler "extends" it to a real class. Thus, we have all the advantages (and sometimes disadvantages) of a real strongly typed language.

Here is 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'; } ); } 

Example: @WandBox

You can also check out CppInsights, which shows how the compiler extends the code:

Take a look at this example:

CppInsighs: lambda test

In this example, the compiler converts:

 [] (int x) { std::cout << x << '\n'; } 


In something like this (simplified form):

 struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; 

Syntax lambda expression:

 [] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | |   |      

Some definitions before we begin:

From [expr.prim.lambda # 2] :

Calculating lambda expressions results in a temporary prvalue. This temporary object is called a closure object .

And from [expr.prim.lambda # 3] :

The type of lambda expression (which is also the type of the closure object) is a unique nameless non-union class type, which is called the closure type .

Some examples of lambda expressions are:

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; } 

Lambda type

Since the compiler generates a unique name for each lambda, it is not possible to recognize it in advance.

 auto myLambda = [](int a) -> double { return 2.0 * a; } 

Moreover [expr.prim.lambda] :
The type of closure associated with a lambda expression has a remote ([dcl.fct.def.delete]) default constructor and an assignment operator.

Therefore you cannot write:

 auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy; 

This will result in the following error in GCC:

 error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor 

Call operator

The code that you put in the body of the lambda is “translated” into the operator () code of the corresponding type of closure.

By default, this is a built-in constant method. You can change it by specifying mutable after declaring parameters:

 auto myLambda = [](int a) mutable { std::cout << a; } 

Although the constant method is not a “problem” for lambda without an empty capture list ... it matters when you want to capture something.

Capture

[] not only introduces lambda, but also contains a list of captured variables. This is called a "capture list."

By capturing a variable, you create a member copy of this variable in the closure type. Then inside the body of the lambda you can access it.

The basic syntax is:


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 around with a complete example here: @Wandbox

Although specifying [=] or [&] may be convenient - since it captures all variables in automatic storage, it is more obvious to capture variables explicitly. Thus, the compiler can warn you about unwanted effects (see, for example, notes on global and static variables)

You can also read more in paragraph 31 of Scott Meyers Effective Modern C ++: “Avoid default capture modes.”

And an important quote:
C ++ closures do not increase the lifetime of captured links.


Mutable

By default, operator () of the closure type is constant, and you cannot change the captured variables inside the body of the lambda expression.
If you want to change this behavior, you need to add the 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 example above, we can change the values ​​of x and y ... but these are only copies of x and y from the attached scope.

Global variable capture

If you have a global value, and then you use [=] in lambda, you might think that the global value is also captured by value ... but it is 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 the code here: @Wandbox

Only variables in automatic storage are captured. GCC may even issue 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 will not help you.
The Clang compiler is more useful because it generates an error:

 error: 'global' cannot be captured because it does not have automatic storage duration 

See @Wandbox

Capturing Static Variables

Capturing static variables is similar to capturing global variables:

 #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 the code here: @Wandbox

Conclusion:

 10 11 12 

Again, a warning will appear only if you explicitly capture a static variable, so if you use [=] , the compiler will not help you.

Class member grabbing

Do you know what will happen after executing 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 calls foo() . 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 we call f1 and f2. This is a hanging link problem that gives rise to undefined behavior.

Similarly:

 struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo(); //   

Play with the @Wandbox code

Again, if you specify the capture explicitly ([s]):

 std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; } 

The compiler will prevent your error:

 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 an example: @Wandbox

Move-able-only objects

If you have an object that can only be moved (for example, unique_ptr), then you cannot put it in the lambda as a captured variable. Capture by value does not work, so you can only capture by reference ... however, this will not give you possession, and probably this is not what you wanted.

 std::unique_ptr<int> p(new int{10}); auto foo = [p] () {}; //  .... 

Saving constants

If you capture a constant variable, then the constancy is preserved:

 int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo(); 

See code: @Wandbox

Return type

In C ++ 11, you can skip the trailing return type of lambda, and then the compiler will output it for you.

Initially, the output of the return type was limited to lambdas containing a single return statement, but this restriction was quickly removed, since there were no problems with implementing a more convenient version.

See C ++ Standard Core Language Defect Reports and Accepted Issues (thanks to Thomas for finding the right link!)

Thus, starting with C ++ 11, the compiler can infer the type of the return value if all return operators can be converted to the same type.
If all return statements return an expression and the types of expressions returned after the conversion, lvalue-to-rvalue (7.1 [conv.lval]), array-to-pointer (7.2 [conv.array]), and function-to-pointer (7.3 [conv. func]) is the same as the general type;
 auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; }; 

Play around with the code here: @Wandbox

There are two return in the above lambda, but they all indicate double , so the compiler can infer the type.

IIFE - Immediately Called Expressions (Immediately Invoked Function Expression)

In our examples, I defined a lambda, and then called it using the closure object ... but it can also be called immediately:

 int x = 1, y = 1; [&]() { ++x; ++y; }(); // <-- call () std::cout << x << " " << y << std::endl; 

Such an expression can be useful for complex initialization of constant objects.

 const auto val = []() { /*   ... */ }(); 

I wrote more about this in post IIFE for Complex Initialization .

Conversion to function pointer
The closure type for a non-capture lambda expression has an open non-virtual implicit constant conversion function to a pointer to a function that has the same parameter and return types as the closure type function call operator. The value returned by this conversion function must be the address of a function that, when called, has the same effect as calling a function operator of a type similar to a closure type.
In other words, you can convert 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 around with the code here: @Wandbox

Improvements in C ++ 14

Standard N4140 and lambda: [expr.prim.lambda] .

C ++ 14 added two significant improvements to lambda expressions:


These features solve several problems that were visible in C ++ 11.

Return type

The output type of the return value of the lambda expression has been updated to conform to the automatic inference rules for functions.

[expr.prim.lambda # 4]
The lambda return type is auto, which is replaced by the trailing return type if it is provided and / or derived from return statements, as described in [dcl.spec.auto].
Captures with initializer

In short, we can create a new member variable of closure type and then use it inside a lambda expression.

For example:

 int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); } 

This can solve several problems, for example, with types that are only available for movement.

Move

Now we can move the object to the closure type member:

 #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)] {}; } 

Optimization

Another idea is to use it as a potential optimization technique. Instead of calculating some value each time we call a lambda, we can calculate it once in the initializer:

 #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; }); } 

Member variable capture

The initializer can also be used to capture a member variable. Then we can get a copy of a member variable and not worry about hanging links.

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 around with the code here: @Wandbox


In foo() we capture a member variable by copying it into a closure type. In addition, we use auto to output the entire method (previously, in C ++ 11 we could use std::function ).

Generalized lambda expressions

Another significant improvement is generalized lambda.
Starting with C ++ 14 you can write:

 auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world"); 

This is equivalent to using a pattern declaration in a closure type call statement:

 struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance; 

Such a generalized lambda can be very useful when it is difficult to infer a type.

For example:

 std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } }; //      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'; } ); 

Am I mistaken here? Does entry have the correct type?
.
.
.
Probably not, since the value type for std :: map is std::pair<const Key, T> . So my code will make extra copies of the lines ...
This can be fixed with auto :

 std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

You can play around with the code here: @Wandbox

Conclusion

What a story!

In this article, we started from the first days of lambda expressions in C ++ 03 and C ++ 11 and moved on to an improved version in C ++ 14.

You saw how to create a lambda, what is the basic structure of this expression, what a capture list is and much more.

In the next part of the article, we will move on to C ++ 17 and get acquainted with the future features of C ++ 20.

The second part is available here:

Lambdas: From C ++ 11 to C ++ 20, Part 2


Links

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
Lambda Expressions in C ++ | Microsoft docs
Demystifying C ++ lambdas - Sticky Bits - Powered by FeabhasSticky Bits - Powered by Feabhas


We are waiting for your comments and invite all interested to the course "Developer C ++" .

Source: https://habr.com/ru/post/444524/


All Articles