Параллельное программирование на С++ в действии — страница 37 из 53

Лямбда-функции — одно из самых интересных новшеств в стандарте C++11, потому что они позволяют существенно упростить код и исключить многие стереотипные конструкции, которые применяются при написании объектов, допускающих вызов. Синтаксис лямбда-функций в C++11 позволяет определить функцию в той точке выражения, где она необходима. Это отличное решение, например, для передачи предикатов функциям ожидания из класса

std::condition_variable
(как в примере из раздела 4.1.1), потому что дает возможность кратко выразить семантику в терминах доступных в данной точке переменных, а не запоминать необходимое состояние в переменных-членах класса с оператором вызова.

В простейшем случае лямбда-выражение определяет автономную функцию без параметров, которая может пользоваться только глобальными переменными и функциями. У нее даже нет возвращаемого значения. Такое лямбда-выражение представляет собой последовательность предложений, заключенных в фигурные скобки, которым предшествуют квадратные скобки (так называемый лямбда-интродуктор):

[] { ←
Лямбда-выражение начинается с []

 do_stuff();     │
Конец определения

 do_more_stuff();│
лямбда-выражения

} ();           ←┘
и его вызов

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

std::cout
, разделяя их символами новой строки:

std::vector data = make_data();

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

 [](int i){std::cout << i << "\n";});

С возвращаемыми значениями всё почти так же просто. Если тело лямбда-функции состоит из единственного предложения

return
, то тип возвращаемого ей значения совпадает с типом возвращаемого выражения. Например, такую простую лямбда-функцию можно было бы использовать для проверки флага, ожидаемого условной переменной
std::condition_variable
(см. раздел 4.1.1).


Листинг А.4. Простая лямбда-функция с выводимым типом возвращаемого значения

std::condition_variable cond;

 bool data_ready;

 std::mutex m;


void wait_for_data() {

 std::unique_lock lk(m);

 cond.wait(lk, []{return data_ready;}); ←
(1)

}

Тип значения, возвращаемого лямбда-функцией, которая передана

cond.wait()
(1), выводится из типа переменной
data_ready
, то есть совпадает с
bool
. Когда условная переменная получает сигнал, она вызывает эту лямбда-функцию, захватив предварительно мьютекс, и
wait()
возвращает управление, только если
data_ready
равно
true
.

Но что если невозможно написать тело лямбда-функции, так чтобы оно содержало единственное предложение

return
? В таком случае тип возвращаемого значения следует задать явно. Это можно сделать и тогда, когда тело функции состоит из единственного предложения
return
, но обязательно, если тело более сложное. Для задания типа возвращаемого значения нужно поставить после списка параметров функции стрелку (
->
), а за ней указать тип. Если лямбда-функция не имеет параметров, то список параметров (пустой) все равно необходим, иначе задать тип возвращаемого значения невозможно. Таким образом, предикат, проверяемый условной переменной, можно записать так:

cond.wait(lk, []()->bool{ return data_ready; });

Лямбда-функции с явно заданным типом возвращаемого значения можно использовать, например, для записи сообщений в журнал или для более сложной обработки:

cond.wait(lk, []()->bool {

 if (data_ready) {

  std::cout << "Данные готовы" << std::endl;

  return true;

 } else {

  std::cout <<

   "Данные не готовы, продолжаю ждать" << std::endl;

  return false;

 }

});

Даже такие простые лямбда-функции весьма полезны и существенно упрощают код, но их истинная мощь проявляется, когда требуется запомнить локальные переменные.

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

Лямбда-функции с лямбда-интродуктором вида

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

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

std::function make_offseter(int offset) {

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

}

При каждом вызове

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

int main() {

 std::function offset_42 = make_offseter(42);

 std::function 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 offset_a =

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

 offset = 123;                   ←
(3)

 std::function 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 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 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 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& 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).

А.6. Шаблоны с переменным числом параметров