Практический курс по C/C++: https://stepik.org/course/193691
Пришло время нам
узнать, для чего нужны и как используются квадратные скобки, стоящие вначале
каждого лямбда-выражения. Во всех предыдущих примерах они у нас с вами были
пустыми. Но, в действительности, через них можно передавать различные
переменные из внешней области видимости, то есть, переменные, находящиеся за
пределами тела лямбда-функции. Этот процесс в С++ называется захват
переменных. И сейчас мы с вами подробно разберемся, как это работает.
Первое, что
вытекает из необходимости захвата внешних значений, это недоступность
переменных, объявленных за пределами лямбда-выражения. Действительно,
компиляция следующего текста программы приведет к ошибке:
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
const int max_size = 1000;
int main()
{
int data[] {1, 2, 3, 4, 5, 6, 7, 8};
size_t sz = sizeof(data)/sizeof(*data);
auto r = []() {
cout << sz << endl; // ошибка, sz не существует
cout << max_size << endl;
};
r();
return 0;
}
Внутри тела
лямбда-функции локальные внешние переменные оказываются недоступными. Но не
глобальные. Их по-прежнему можно использовать в любом месте текущего модуля, в
том числе, и внутри лямбда-выражений.
Давайте поправим
программу и сделаем так, чтобы в объекте r были доступны
все локальные переменные функции main(). Для этого в квадратных скобках
достаточно прописать символ ‘=’ следующим образом:
auto r = [=]() {
cout << sz << endl; // ok
cout << max_size << endl;
};
Этот символ
означает, что мы копируем значения всех переменных в константные переменные,
которые автоматически создаются внутри тела лямбда-функции с теми же самыми
именами. В частности, sz – это новая константная переменная с
копией значения переменной sz из функции main(). Поэтому, при
выводе ее значения, мы видим число 8 и изменить ее уже нельзя. Следующий код не
скомпилируется:
auto r = [=]() {
sz++; // ошибка, sz – константа
cout << sz << endl; // ok
cout << max_size << endl;
};
Однако это
поведение можно изменить, если после круглых скобок лямбда-выражения прописать
ключевое слово mutable следующим образом:
auto r = [=]() mutable {
sz++; // ok
cout << sz << endl; // ok
cout << max_size << endl;
};
Теперь мы имеем
копии всех внешних переменных внутри объекта r с возможностью
их изменения. Это же касается и более сложных данных, например, массивов:
auto r = [=]() mutable {
for(int& x : data) {
x += 2;
cout << x << " ";
}
cout << endl;
};
Если нам нужно
передавать не все переменные, а лишь некоторые, то вместо символа ‘=’ в
квадратных скобках просто прописываются захватываемые переменные, например,
так:
auto r = [sz]() mutable {
cout << sz << endl;
};
В результате, в
теле лямбда-функции будет доступна только переменная sz. А если
прописать так:
auto r = [sz, data]() mutable {
cout << sz << endl;
for(int x : data)
cout << x << " ";
cout << endl;
};
то обе
переменные: sz и data. При этом за
пределами лямбда-функции все переменные остаются неизменными.
Захват переменных по ссылке и указателю
Если же нам
нужно внутри лямбда-выражения оперировать непосредственно внешними локальными
переменными, то их следует передавать либо по ссылке, либо через указатели. Начнем
с захвата внешних переменных по ссылкам. Для этого в квадратных скобках вместо
символа ‘=’ прописывается символ ссылки ‘&’ следующим образом:
auto r = [&]() {
cout << sz++ << endl;
for(int& x : data)
cout << ++x << " ";
cout << endl;
};
Переменные sz и data представляют
собой ссылки на соответствующие переменные. Мы можем совершенно спокойно менять
их внутри лямбда-функции, меняя и внешние переменные.
Также вместо
символа ‘&’ можно указывать отдельные захватываемые переменные. Например:
auto r = [&sz, &data]() {
cout << sz++ << endl;
for(int& x : data)
cout << ++x << " ";
cout << endl;
};
Здесь работа
ведется только с двумя внешними переменными sz и data через
соответствующие ссылки.
Наконец,
похожего эффекта можно добиться, используя указатели на переменные. Например, так:
int main()
{
int data[] {1, 2, 3, 4, 5, 6, 7, 8};
size_t sz = sizeof(data)/sizeof(*data);
size_t *ptr_sz = &sz;
auto r = [ptr_sz, &data]() {
(*ptr_sz)++;
cout << *ptr_sz << endl;
for(int& x : data)
cout << ++x << " ";
cout << endl;
};
r();
cout << sz << endl;
for(int x : data)
cout << x << " ";
cout << endl;
return 0;
}
В результате мы
захватываем указатель ptr_sz и внутри тела
лямбда-функции формируется константный указатель с тем же именем ptr_sz. Через этот
указатель мы совершенно спокойно можем читать и менять значение переменной sz, но не можем
менять адрес указателя ptr_sz, т.к. он
константный.
Этот пример
также показывает, что в квадратных скобках мы можем комбинировать запись через
присваивание и через ссылки. Мало того, можно даже использовать и такие виды
записей:
- [&a, b, &m, n]
// a и m – по ссылке; b и n – по
значению
- [=, &m,
&n] // все по значению; m и n – по ссылке
- [&, m,
n] // все по ссылке; m и n – по значению
Вот так,
относительно просто, можно объявлять, вызывать и использовать лямбда-выражения
в языке С++.
Практический курс по C/C++: https://stepik.org/course/193691