A.5.1. Лямбда-функции, ссылающиеся на локальные переменные

We use cookies. Read the Privacy and Cookie Policy

Лямбда-функции с лямбда-интродуктором вида [] не могут ссылаться на локальные переменные из объемлющей области видимости; им разрешено использовать только глобальные переменные и то, что передано в параметрах. Чтобы получить доступ к локальной переменной, ее нужно захватить (capture). Проще всего захватить все переменные в локальной области видимости, указав лямбда-интродуктор вида [=]. Теперь лямбда-функция может получить доступ к копиям локальных переменных на тот момент, когда эта функция была создана.

Рассмотрим этот механизм на примере следующей простой функции:

std::function<int(int)> make_offseter(int offset) {

 return [=](int j){return offset+j;};

}

При каждом вызове make_offseter с помощью обертки std::function<> создается новый содержащий лямбда-функцию объект. Возвращенная функция добавляет указанное смещение к любому переданному ей параметру. Например, следующая программа

int main() {

 std::function<int(int)> offset_42 = make_offseter(42);

 std::function<int(int)> offset_123 = make_offseter(123);

 std::cout <<

  offset_42(12) << "," << offset_123(12) << std::endl;

 std::cout <<

  offset_42(12) << "," << offset_123(12) << std::endl;

}

два раза выведет числа 54, 135, потому что функция, возвращенная после первого обращения к make_offseter, всегда добавляет 42 к переданному ей аргументу Напротив, функция, возвращенная после второго обращения к make_offseter, добавляет к своему аргументу 123. Это самый безопасный вид захвата локальных переменных — все значения копируются, поэтому лямбда-функцию можно вернуть и вызывать вне контекста функции, в которой она была создана. Но это не единственно возможное решение, можно захватывать локальные переменные и по ссылке. В таком случае попытка вызвать лямбда-функцию после того, как переменные, на которые указывают ссылки, были уничтожены в результате выхода из области видимости объемлющей их функции или блока, приведёт к неопределённому поведению, точно так же, как обращение к уничтоженной переменной в любом другом случае.

Лямбда-функция, захватывающая все локальные переменные по ссылке, начинается интродуктором [&]:

int main() {

 int offset = 42;                ← (1)

 std::function<int(int)> offset_a =

  [&](int j){return offset + j;};← (2)

 offset = 123;                   ← (3)

 std::function<int(int)> offset_b =

  [&](int j){return offset + j;};← (4)

 std::cout <<

  offset_a(12) << "," << offset_b(12) << std::endl; ← (5)

 offset = 99;                                       ← (6)

 std::cout <<

  offset_a(12) << "," << offset_b(12) << std::endl; ← (7)

}

Если функция make_offseter из предыдущего примера захватывала копию смещения offset, то функция offset_a в этом примере, начинающаяся интродуктором [&], захватывает offset по ссылке (2). Неважно, что начальное значение offset было равно 42 (1); результат вызова offset_a(12) зависит от текущего значения offset. Значение offset было изменено на 123 (3) перед порождением второй (идентичной) лямбда-функции offset_b (4), но эта вторая функция снова производит захват по ссылке, поэтому результат, как и прежде, зависит от текущего значения offset.

Теперь при печати первой строки (5), offset всё еще равно 123, поэтому печатаются числа 133, 135. Однако к моменту печати второй строки (7) offset стало равно 99 (6), поэтому печатается 111, 111. И offset_a, и offset_b прибавляют текущее значение offset (99) к переданному аргументу (12).

Но ведь это С++, поэтому вам не обязательно выбирать между всем или ничем; вполне можно захватывать одни переменные по значению, а другие по ссылке. Более того, можно даже указывать, какие именно переменные захватить. Нужно лишь изменить лямбда-интродуктор. Если требуется скопировать все видимые переменные, кроме одной-двух, то воспользуйтесь интродуктором [=], но после знака равенства перечислите переменные, захватываемые по ссылке, предпослав им знаки амперсанда. В следующем примере печатается 1239, потому что переменная i копируется в лямбда-функцию, a j и k захватываются по ссылке:

int main() {

 int i=1234, j=5678, k=9;

 std::function<int()> f=[=,&j,&k] {return i+j+k;};

 i = 1;

 j = 2;

 k = 3;

 std::cout << f() << std::endl;

}

Можно поступить и наоборот — по умолчанию захватывать по ссылке, но некоторое подмножество переменных копировать. В таком случае воспользуйтесь интродуктором [&], а после знака амперсанда перечислите переменные, захватываемые по значению. В следующем примере печатается 5688, потому что i захватывается по ссылке, a j и k копируются:

int main() {

 int i=1234, j=5678, k= 9;

 std::function<int()> f=[&,j,k] {return i+j+k;};

 i = 1;

 j = 2;

 k = 3;

 std::cout << f() << std::endl;

}

Если требуется захватить только именованные переменные, то можно опустить знак = или & и просто перечислить захватываемые переменные, предпослав знак амперсанда тем, что должны захватываться по ссылке, а не по значению. В следующем примере печатается 5682, потому что i и k захвачены по ссылке, a j скопирована

int main() {

 int i=1234, j=5678, k=9;

 std::function<int()> f=[&i, j, &k] {return i+j+k;};

 i = 1;

 j = 2;

 k = 3;

 std::cout << f() << std::endl;

}

Последний способ заодно гарантирует, что захвачены только необходимые переменные, потому что ссылка на локальную переменную, отсутствующую в списке захвата, приведёт к ошибке компиляции. Выбирая этот вариант, нужно соблюдать осторожность при доступе к членам класса, если лямбда-функция погружена в функцию-член класса. Члены класса нельзя захватывать непосредственно; если к ним необходим доступ из лямбда-функции, то необходимо захватить указатель this, включив его в список захвата. В следующем примере лямбда-функция захватывает this для доступа к члену класса some_data:

struct X {

 int some_data;

 void foo(std::vector<int>& vec) {

  std::for_each(vec.begin(), vec.end(),

   [this](int& i){ i += some_data; });

 }

};

В контексте параллелизма лямбда-функции особенно полезны для задания предикатов функции std::condition_variable::wait() (см. раздел 4.1.1) и в сочетании с std::packaged_task<> (раздел 4.2.1) или пулами потоков для упаковки небольших задач. Их можно также передавать конструктору std::thread в качестве функций потока (раздел 2.1.1) и в качестве исполняемой функции в таких параллельных алгоритмах, как parallel_for_each() (раздел 8.5.1).