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

Section 73.7: Capture by reference

If you precede a local variable's name with an &, then the variable will be captured by reference. Conceptually, this means that the lambda's closure type will have a reference variable, initialized as a reference to the corresponding variable from outside of the lambda's scope. Any use of the variable in the lambda body will refer to the original variable:

//Declare variable 'a' int a = 0;

//Declare a lambda which captures 'a' by reference auto set = [&a]() {

a = 1;

};

set(); assert(a == 1);

The keyword mutable is not needed, because a itself is not const.

Of course, capturing by reference means that the lambda must not escape the scope of the variables it captures. So you could call functions that take a function, but you must not call a function that will store the lambda beyond the scope of your references. And you must not return the lambda.

Section 73.8: Generic lambdas

Version ≥ c++14

Lambda functions can take arguments of arbitrary types. This allows a lambda to be more generic:

auto twice = [](auto x){ return x+x; };

int i = twice(2); // i == 4

std::string s = twice("hello"); // s == "hellohello"

This is implemented in C++ by making the closure type's operator() overload a template function. The following type has equivalent behavior to the above lambda closure:

struct _unique_lambda_type

{

template<typename T>

auto operator() (T x) const {return x + x;}

};

Not all parameters in a generic lambda need be generic:

[](auto x, int y) {return x + y;}

Here, x is deduced based on the first function argument, while y will always be int.

Generic lambdas can take arguments by reference as well, using the usual rules for auto and &. If a generic parameter is taken as auto&&, this is a forwarding reference to the passed in argument and not an rvalue reference:

auto lamb1 = [](int &&x) {return x + 5;}; auto lamb2 = [](auto &&x) {return x + 5;}; int x = 10;

GoalKicker.com – C++ Notes for Professionals

390

lamb1(x); // Illegal; must use `std::move(x)` for `int&&` parameters. lamb2(x); // Legal; the type of `x` is deduced as `int&`.

Lambda functions can be variadic and perfectly forward their arguments:

auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);};

or:

auto lam = [](auto&&... args){return f(decltype(args)(args)...);};

which only works "properly" with variables of type auto&&.

A strong reason to use generic lambdas is for visiting syntax.

boost::variant<int, double> value; apply_visitor(value, [&](auto&& e){

std::cout << e; });

Here we are visiting in a polymorphic manner; but in other contexts, the names of the type we are passing isn't interesting:

mutex_wrapped<std::ostream&> os = std::cout; os.write([&](auto&& os){

os << "hello world\n"; });

Repeating the type of std::ostream& is noise here; it would be like having to mention the type of a variable every time you use it. Here we are creating a visitor, but no a polymorphic one; auto is used for the same reason you might use auto in a for(:) loop.

Section 73.9: Using lambdas for inline parameter pack unpacking

Version ≥ C++14

Parameter pack unpacking traditionally requires writing a helper function for each time you want to do it.

In this toy example:

template<std::size_t...Is>

void print_indexes( std::index_sequence<Is...> ) { using discard=int[];

(void)discard{0,((void)(

std::cout << Is << '\n' // here Is is a compile-time constant. ),0)...};

}

template<std::size_t I> void print_indexes_upto() {

return print_indexes( std::make_index_sequence<I>{} );

}

The print_indexes_upto wants to create and unpack a parameter pack of indexes. In order to do so, it must call a helper function. Every time you want to unpack a parameter pack you created, you end up having to create a

GoalKicker.com – C++ Notes for Professionals

391

custom helper function to do it.

This can be avoided with lambdas.

You can unpack parameter packs into a set of invocations of a lambda, like this:

template<std::size_t I>

using index_t = std::integral_constant<std::size_t, I>; template<std::size_t I>

constexpr index_t<I> index{};

template<class=void, std::size_t...Is>

auto index_over( std::index_sequence<Is...> ) { return [](auto&& f){

using discard=int[]; (void)discard{0,(void(

f( index<Is> ) ),0)...};

};

}

template<std::size_t N>

auto index_over(index_t<N> = {}) {

return index_over( std::make_index_sequence<N>{} );

}

Version ≥ C++17

With fold expressions, index_over() can be simplified to:

template<class=void, std::size_t...Is>

auto index_over( std::index_sequence<Is...> ) { return [](auto&& f){

((void)(f(index<Is>)), ...);

};

}

Once you have done that, you can use this to replace having to manually unpack parameter packs with a second overload in other code, letting you unpack parameter packs "inline":

template<class Tup, class F>

void for_each_tuple_element(Tup&& tup, F&& f) { using T = std::remove_reference_t<Tup>;

using std::tuple_size;

auto from_zero_to_N = index_over< tuple_size<T>{} >();

from_zero_to_N( [&](auto i){

using std::get;

f( get<i>( std::forward<Tup>(tup) ) );

}

);

}

The auto i passed to the lambda by the index_over is a std::integral_constant<std::size_t, ???>. This has a constexpr conversion to std::size_t that does not depend on the state of this, so we can use it as a compile-time constant, such as when we pass it to std::get<i> above.

To go back to the toy example at the top, rewrite it as:

GoalKicker.com – C++ Notes for Professionals

392

template<std::size_t I> void print_indexes_upto() {

index_over(index<I>)([](auto i){

std::cout << i << '\n'; // here i is a compile-time constant });

}

which is much shorter, and keeps logic in the code that uses it.

Live example to play with.

Section 73.10: Generalized capture

Version ≥ C++14

Lambdas can capture expressions, rather than just variables. This permits lambdas to store move-only types:

auto p = std::make_unique<T>(...);

auto lamb = [p = std::move(p)]() //Overrides capture-by-value of `p`.

{

p->SomeFunc();

};

This moves the outer p variable into the lambda capture variable, also called p. lamb now owns the memory allocated by make_unique. Because the closure contains a type that is non-copyable, this means that lamb is itself non-copyable. But it can be moved:

auto lamb_copy = lamb; //Illegal

auto lamb_move = std::move(lamb); //legal.

Now lamb_move owns the memory.

Note that std::function<> requires that the values stored be copyable. You can write your own move-only- requiring std::function, or you could just stu the lambda into a shared_ptr wrapper:

auto shared_lambda = [](auto&& f){

return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))] (auto&&...args)->decltype(auto) {

return (*spf)(decltype(args)(args)...);

};

};

auto lamb_shared = shared_lambda(std::move(lamb_move));

takes our move-only lambda and stu s its state into a shared pointer then returns a lambda that can be copied, and then stored in a std::function or similar.

Generalized capture uses auto type deduction for the variable's type. It will declare these captures as values by default, but they can be references as well:

int a = 0;

auto lamb = [&v = a](int add) //Note that `a` and `v` have different names

{

v += add; //Modifies `a`

};

GoalKicker.com – C++ Notes for Professionals

393