C++17 STL Стандартная библиотека шаблонов — страница 33 из 119


Как это делается


В этом примере мы напишем небольшую программу, в которой поработаем с лямбда-выражениями, чтобы понять основные принципы взаимодействия с ними.


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


#include 

#include 


2. В данном примере все действие происходит в функции

main
. Мы определим два объекта функций, которые не принимают параметры, и вернем целочисленные константы со значениями
1
и
2
. Обратите внимание: выражение
return
окружено фигурными скобками
{}
, как это делается в обычных функциях, а круглые скобки
()
, указывающие на функцию без параметров, являются необязательными, мы не указываем их во втором лямбда-выражении. Но квадратные скобки
[]
должны присутствовать:


int main()

{

  auto just_one ( [](){ return 1; } );

  auto just_two ( []  { return 2; } );


3. Теперь можно вызвать оба объекта функций, просто написав имя переменных, которые в них сохранены, и добавив скобки. В этой строке их не отличить от обычных функций:


  std::cout << just_one() << ", " << just_two() << '\n';


4. Забудем о них и определим еще один объект функции, который называется

plus
, — он принимает два параметра и возвращает их сумму:


  auto plus ( [](auto l, auto r) { return l + r; } );


5. Использовать такой объект довольно просто, в этом плане он похож на любую другую бинарную функцию. Мы указали, что его параметры имеют тип auto, вследствие чего объект будет работать со всеми типами данных, для которых определен оператор +, например со строками.


  std::cout << plus(1, 2) << '\n';

  std::cout << plus(std::string{"a"}, "b") << '\n';


6. Не нужно сохранять лямбда-выражение в переменной, чтобы использовать его. Мы также можем определить его в том месте, где это необходимо, а затем разместить параметры для данного выражения в круглых скобках сразу после него (1, 2):


  std::cout

<< [](auto l, auto r){ return l + r; }(1, 2)

<< '\n';


7. Далее определим замыкание, которое содержит целочисленный счетчик. При каждом вызове значение этого счетчика будет увеличиваться на

1
и возвращать новое значение. Для указания на то, что замыкание содержит внутренний счетчик, разместим в скобках выражение
count = 0
— оно указывает, что переменная
count
инициализирована целочисленным значением
0
. Чтобы позволить ему изменять собственные переменные, мы используем ключевое слово
mutable
, поскольку в противном случае компилятор не разрешит это сделать:


  auto counter (

    [count = 0] () mutable { return ++count; }

  );


8. Теперь вызовем объект функции пять раз и выведем возвращаемые им значения с целью увидеть, что значение счетчика увеличивается:


  for (size_t i {0}; i < 5; ++i) {

    std::cout << counter() << ", ";

  }

  std::cout << '\n';


9. Мы также можем взять существующие переменные и захватить их по ссылке вместо того, чтобы создавать копию значения для замыкания. Таким образом, значение переменной станет увеличиваться в замыкании и при этом будет доступно за его пределами. Для этого мы поместим в скобках конструкцию

&a
, где символ
&
означает, что мы сохраняем ссылку на переменную, но не копию:


  int a {0};

  auto incrementer ( [&a] { ++a; } );


10. Если это работает, то можно вызвать данный объект функции несколько раз, а затем пронаблюдать, действительно ли меняется значение переменной

a
:


  incrementer();

  incrementer();

  incrementer();


  std::cout

<< "Value of 'a' after 3 incrementer() calls: "

<< a << '\n';


11. Последний пример демонстрирует каррирование. Оно означает, что мы берем функцию, принимающую некоторые параметры, а затем сохраняем ее в другом объекте функции, принимающем меньше параметров. В этом случае мы сохраняем функцию

plus
и принимаем только один параметр, который будет передан в функцию
plus
. Другой параметр имеет значение
10
; его мы сохраняем в объекте функции. Таким образом, мы получаем функцию и назовем ее
plus_ten
, поскольку она может добавить значение
10
к единственному принимаемому ею параметру.


  auto plus_ten ( [=] (int x) { 
return plus(10, x);});

    std::cout << plus_ten(5) << '\n';

}


12. Перед компиляцией и запуском программы пройдем по коду еще раз и попробуем предугадать, какие именно значения выведем в терминале. Затем запустим программу и взглянем на реальные выходные данные:


1, 2

3

ab 3

1, 2, 3, 4, 5,

Value of a after 3 incrementer() calls: 3

15


Как это работает

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

Итак, сначала рассмотрим все особенности, связанные с лямбда-выражениями (рис. 4.1).

Как правило, можно опустить большую часть этих параметров, чтобы сэкономить немного времени. Самым коротким лямбда-выражением является выражение

[]{}
. Оно не принимает никаких параметров, ничего не захватывает и, по сути, ничего не делает.

Что же значит остальная часть?


Список для захвата

Определяет, что именно мы захватываем и выполняем ли захват вообще. Есть несколько способов сделать это. Рассмотрим два «ленивых» варианта.

1. Если мы напишем

[=]
()
{...}
, то захватим каждую внешнюю переменную, на которую ссылается замыкание, по значению; т.е. эти значения будут скопированы.

2. Запись

[&]
()
{...}
означает следующее: все внешние объекты, на которые ссылается замыкание, захватываются только по ссылке, что не приводит к копированию.


Конечно, можно установить настройки захвата для каждой переменной отдельно. Запись

[a, &b]
()
{...}
означает, что переменную
a
мы захватываем по значению, а переменную
b
по ссылке. Для этого потребуется напечатать больше текста, но, как правило, данный способ безопаснее, поскольку мы не можем случайно захватить что-то ненужное из-за пределов замыкания.

В текущем примере мы определили лямбда-выражение следующим образом:

[count=0]
()
{...}
. В этом особом случае мы не захватываем никаких переменных из-за пределов замыкания, только определили новую переменную с именем
count
. Тип данной переменной определяется на основе значения, которым мы ее инициализировали, а именно
0
, так что она имеет тип
int
.

Кроме того, можно захватить одни переменные по значению, а другие — по ссылке, например:

[a, &b]
()
{...}
— копируем
a
и берем ссылку на
b
;

[&, a]
()
{...}
— копируем a и применяем ссылку на любую другую переданную переменную;

[=, &b, i{22}, this]
()
{...}
— получаем ссылку на
b
, копируем значение
this
, инициализируем новую переменную
i
значением
22
и копируем любую другую использованную переменную.