A.5.1. Лямбда-функции, ссылающиеся на локальные переменные
Лямбда-функции с лямбда-интродуктором вида [] не могут ссылаться на локальные переменные из объемлющей области видимости; им разрешено использовать только глобальные переменные и то, что передано в параметрах. Чтобы получить доступ к локальной переменной, ее нужно захватить (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).