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
и копируем любую другую использованную переменную.
[member_a]
()
{...}
. Вместо этого нужно определить либо this
, либо *this
.
mutable (необязательный)
Если объект функции должен иметь возможность [=]
), то его следует определить как mutable
. Это же касается вызова неконстантных методов захваченных объектов.
constexpr (необязательный)
Если мы явно пометим лямбда-выражение с помощью ключевого слова constexpr
, то компилятор сгенерирует constexpr
. Преимущество использования функций constexpr
и лямбда-выражений заключается в том, что компилятор может оценить их результат во время компиляции, если они вызываются с параметрами, постоянными на протяжении данного процесса. Это приведет к тому, что позднее в бинарном файле будет меньше кода.
Если мы не указываем явно, что лямбда-выражения являются constexpr
, но эти выражения соответствуют всем требуемым критериям, то они все равно будут считаться constexpr
, только constexpr
, то лучше явно задавать его таковым, поскольку иначе в случае наших
exception attr (необязательный)
Здесь определяется, может ли объект функции генерировать исключения, если при вызове столкнется с ошибкой.
return type (необязательный)
При необходимости иметь полный контроль над возвращаемым типом, вероятно, не нужно, чтобы компилятор определял его автоматически. В таких случаях можно просто использовать конструкцию [] () -> Foo {}
, которая укажет компилятору, что мы всегда будем возвращать объекты типа Foo
.
Добавляем полиморфизм путем оборачивания лямбда-выражений в std::function