Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

The const applies to accesses to member variables of the closure type, and captured variables that are members of the closure (all appearances to the contrary):

int a = 0;

[a]() {

a = 2; // Illegal, 'a' is accessed via `const`

decltype(a) a1 = 1;

a1 = 2; // valid: variable 'a1' is not const

};

To remove the const, you have to specify the keyword mutable on the lambda:

int a = 0;

[a]() mutable {

a = 2; // OK, 'a' can be modified return a;

};

Because a was captured by value, any modifications done by calling the lambda will not a ect a. The value of a was copied into the lambda when it was constructed, so the lambda's copy of a is separate from the external a variable.

int a = 5 ;

auto plus5Val = [a] (void) { return a + 5 ; } ; auto plus5Ref = [&a] (void) {return a + 5 ; } ;

a = 7 ;

std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ; // The result will be "7, value 10, reference 12"

Section 73.4: Recursive lambdas

Let's say we wish to write Euclid's gcd() as a lambda. As a function, it is:

int gcd(int a, int b) {

return b == 0 ? a : gcd(b, a%b);

}

But a lambda cannot be recursive, it has no way to invoke itself. A lambda has no name and using this within the body of a lambda refers to a captured this (assuming the lambda is created in the body of a member function, otherwise it is an error). So how do we solve this problem?

Use std::function

We can have a lambda capture a reference to a not-yet constructed std::function:

std::function<int(int, int)> gcd = [&](int a, int b){ return b == 0 ? a : gcd(b, a%b);

};

This works, but should be used sparingly. It's slow (we're using type erasure now instead of a direct function call), it's fragile (copying gcd or returning gcd will break since the lambda refers to the original object), and it won't work with generic lambdas.

GoalKicker.com – C++ Notes for Professionals

386

Using two smart pointers:

auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>(); *gcd_self = std::make_unique<std::function<int(int, int)>>(

[gcd_self](int a, int b){

return b == 0 ? a : (**gcd_self)(b, a%b);

};

};

This adds a lot of indirection (which is overhead), but it can be copied/returned, and all copies share state. It does let you return the lambda, and is otherwise less fragile than the above solution.

Use a Y-combinator

With the help of a short utility struct, we can solve all of these problems:

template <class F> struct y_combinator {

F f; // the lambda will be stored here

// a forwarding operator(): template <class... Args>

decltype(auto) operator()(Args&&... args) const {

//we pass ourselves to f, then the arguments.

//the lambda should take the first argument as `auto&& recurse` or similar. return f(*this, std::forward<Args>(args)...);

}

};

//helper function that deduces the type of the lambda: template <class F>

y_combinator<std::decay_t<F>> make_y_combinator(F&& f) { return {std::forward<F>(f)};

}

//(Be aware that in C++17 we can do better than a `make_` function)

we can implement our gcd as:

auto gcd = make_y_combinator( [](auto&& gcd, int a, int b){

return b == 0 ? a : gcd(b, a%b);

}

);

The y_combinator is a concept from the lambda calculus that lets you have recursion without being able to name yourself until you are defined. This is exactly the problem lambdas have.

You create a lambda that takes "recurse" as its first argument. When you want to recurse, you pass the arguments to recurse.

The y_combinator then returns a function object that calls that function with its arguments, but with a suitable "recurse" object (namely the y_combinator itself) as its first argument. It forwards the rest of the arguments you call the y_combinator with to the lambda as well.

In short:

auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {

//write body that processes some arguments

//when you want to recurse, call recurse(some other arguments)

GoalKicker.com – C++ Notes for Professionals

387

});

and you have recursion in a lambda with no serious restrictions or significant overhead.

Section 73.5: Default capture

By default, local variables that are not explicitly specified in the capture list, cannot be accessed from within the lambda body. However, it is possible to implicitly capture variables named by the lambda body:

int a = 1; int b = 2;

// Default

capture by

value

[=]() { return a + b;

}; // OK; a and b are captured by value

// Default

capture

by

reference

[&]() { return a +

b;

}; // OK; a and b are captured by reference

Explicit capturing can still be done alongside implicit default capturing. The explicit capture definition will override the default capture:

int a = 0; int b = 1;

[=, &b]() {

a = 2; // Illegal; 'a' is capture by value, and lambda is not 'mutable' b = 2; // OK; 'b' is captured by reference

};

Section 73.6: Class lambdas and capture of this

A lambda expression evaluated in a class' member function is implicitly a friend of that class:

class Foo

{

private: int i;

public:

Foo(int val) : i(val) {}

// definition of a member function void Test()

{

auto lamb = [](Foo &foo, int val)

{

// modification of a private member variable foo.i = val;

};

// lamb is allowed to access a private member, because it is a friend of Foo lamb(*this, 30);

}

};

Such a lambda is not only a friend of that class, it has the same access as the class it is declared within has.

Lambdas can capture the this pointer which represents the object instance the outer function was called on. This

GoalKicker.com – C++ Notes for Professionals

388

is done by adding this to the capture list:

class Foo

{

private: int i;

public:

Foo(int val) : i(val) {}

void Test()

{

// capture the this pointer by value auto lamb = [this](int val)

{

i = val;

};

lamb(30);

}

};

When this is captured, the lambda can use member names of its containing class as though it were in its containing class. So an implicit this-> is applied to such members.

Be aware that this is captured by value, but not the value of the type. It is captured by the value of this, which is a pointer. As such, the lambda does not own this. If the lambda out lives the lifetime of the object that created it, the lambda can become invalid.

This also means that the lambda can modify this without being declared mutable. It is the pointer which is const, not the object being pointed to. That is, unless the outer member function was itself a const function.

Also, be aware that the default capture clauses, both [=] and [&], will also capture this implicitly. And they both capture it by the value of the pointer. Indeed, it is an error to specify this in the capture list when a default is given.

Version ≥ C++17

Lambdas can capture a copy of the this object, created at the time the lambda is created. This is done by adding *this to the capture list:

class Foo

{

private: int i;

public:

Foo(int val) : i(val) {}

void Test()

{

// capture a copy of the object given by the this pointer auto lamb = [*this](int val) mutable

{

i = val;

};

lamb(30); // does not change this->i

}

};

GoalKicker.com – C++ Notes for Professionals

389