Захват внешних значений в лямбда выражениях

Практический курс по 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

Видео по теме