Шрифт:
Интервал:
Закладка:
Начиная с С++11 создавать подобные замыкания стало проще:
#include <iostream>
int main() {
auto greet_john_doe ([] {
std::cout << "Hello, John Doen";
});
greet_john_doe();
}
На этом все. Целая структура name_greeter заменяется небольшой конструкцией [] { /* сделать что-то */ }, которая на первый взгляд выглядит странно, но уже в следующем разделе мы рассмотрим все возможные случаи ее применения.
Лямбда-выражения помогают поддерживать код обобщенным и чистым. Они могут применяться как параметры для обобщенных алгоритмов, чтобы уточнить их при обработке конкретных типов, определенных пользователем. Они также могут служить для оборачивания рабочих пакетов и данных, чтобы их можно было запускать в потоках или просто сохранять работу и откладывать само выполнение пакетов. После появления С++11 было создано множество библиотек, работающих с лямбда-выражениями, поскольку они стали естественной частью языка С++. Еще одним вариантом использования лямбда-выражений является метапрограммирование, поскольку они могут быть оценены во время выполнения программы. Однако мы не будем рассматривать этот вопрос, так как он не относится к теме данной книги.
В текущей главе мы в значительной мере будем опираться на отдельные шаблоны функционального программирования, которые могут показаться странными для новичков и даже опытных программистов, еще не работавших с такими шаблонами. Если в последующих примерах вы увидите лямбда-выражения, возвращающие лямбда-выражения, которые опять-таки возвращают лямбда-выражения, то, пожалуйста, не теряйтесь. Мы несколько выходим за рамки привычного стиля программирования, чтобы подготовиться к работе с современным языком С++, в котором шаблоны функционального программирования встречаются все чаще и чаще. Если код какого-то примера выглядит слишком сложным, то уделите время тому, чтобы подробнее разобрать его. Как только вы с этим разберетесь, сложные лямбда-выражения в реальных проектах больше не будут вас смущать.
Динамическое определение функций с помощью лямбда-выражений
Применяя лямбда-выражения, можно инкапсулировать код, чтобы вызвать его позже или даже в другом месте, поскольку его разрешено копировать. Кроме того, можно инкапсулировать код несколько раз с несколько различающимися параметрами, при этом не нужно будет реализовывать новый класс функции для этой задачи.
Синтаксис лямбда-выражений выглядел новым в С++11, и к С++17 он несколько изменился. В этом разделе мы увидим, как сейчас выглядят лямбда-выражения и что они означают.
Как это делается
В этом примере мы напишем небольшую программу, в которой поработаем с лямбда-выражениями, чтобы понять основные принципы взаимодействия с ними.
1. Для работы с лямбда-выражениями не нужна поддержка библиотек, но мы будем выводить сообщения на консоль и использовать строки, поэтому понадобятся соответствующие заголовочные файлы:
#include <iostream>
#include <string>
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. Последний пример демонстрирует каррирование.