В синтаксисе лямбда-функций, какой целью служит "список захвата"?
В качестве примера, взятого из ответа на этот вопрос, это код, который вычисляет сумму элементов в std::vector
:
std::for_each(
vector.begin(),
vector.end(),
[&](int n) {
sum_of_elems += n;
}
);
Я понимаю, что лямбда-функции - это просто безымянные функции.
Я понимаю синтаксис лямбда-функций как описано здесь.
Я не понимаю, почему для лямбда-функций нужен список захвата, а нормальные функции - нет.
- В какой дополнительной информации содержится список захвата?
- Зачем нормальным функциям не нужна эта информация?
- Являются ли лямбда-функции более чем безымянными функциями?
Ответы
Ответ 1
В приведенной синтаксической ссылке список захвата "определяет, что из-за границы лямбда должно быть доступно внутри тела функции и как"
Обычные функции могут использовать внешние данные несколькими способами:
- Статические поля
- Поля экземпляра
- Параметры (включая контрольные параметры)
- Globals
Lambda добавляет возможность иметь одну неназванную функцию внутри другой. Затем лямбда может использовать указанные вами значения. В отличие от обычных функций, это может включать локальные переменные из внешней функции.
Как говорится в этом ответе, вы также можете указать, как вы хотите захватить. awoodland дает несколько примеров в другом ответе. Например, вы можете захватить одну внешнюю переменную по ссылке (как ссылочный параметр), а все остальные по значению:
[=, &epsilon]
EDIT:
Важно различать подпись и то, что лямбда использует внутри. Подпись лямбда - это упорядоченный список типов параметров, а также тип возвращаемого значения.
Например, унарная функция принимает одно значение определенного типа и возвращает значение другого типа.
Однако внутренне он может использовать другие значения. В качестве тривиального примера:
[x, y](int z) -> int
{
return x + y - z;
}
Вызывающий лямбда знает только, что он принимает int
и возвращает int
. Однако, внутренне это происходит, чтобы использовать две другие переменные по значению.
Ответ 2
Основная проблема, которую мы пытаемся решить, заключается в том, что какой-то алгоритм ожидает функцию, которая принимает только определенный набор аргументов (один int
в вашем примере). Однако мы хотим, чтобы функция могла манипулировать или проверять какой-либо другой объект, возможно, так:
void what_we_want(int n, std::set<int> const & conditions, int & total)
{
if (conditions.find(n) != conditions.end()) { total += n; }
}
Тем не менее, нам разрешено давать наш алгоритм, как функция void f(int)
. Итак, где мы помещаем другие данные?
Вы можете либо сохранить другие данные в глобальной переменной, либо следовать традиционному подходу С++ и написать функтор:
struct what_we_must_write
{
what_we_must_write(std::set<int> const & s, int & t)
: conditions(s), total(t)
{ }
void operator()(int n)
{
if (conditions.find(n) != conditions.end()) { total += n; }
}
private:
std::set<int> const & conditions;
int & total;
};
Теперь мы можем назвать алгоритм подходящим образом инициализированным функтором:
std::set<int> conditions;
int total;
for_each(v.begin(), v.end(), what_we_must_write(conditions, total));
Наконец, объект замыкания (который описывается лямбда-выражением) заключается в следующем: короткий способ записи функтора. Эквивалентом указанного выше функтора является лямбда
auto what_we_get = [&conditions, &total](int n) -> void {
if (condiditons.find(n) != conditions.end()) { total += n; } };
Списки коротких захватов [=]
и [&]
просто захватывают "все" (соответственно по значению или по ссылке), что означает, что компилятор определяет конкретный список захвата для вас (он фактически не ставится все в объект закрытия, но только то, что вам нужно).
Итак, в двух словах: объект замыкания без захвата подобен свободной функции, а замыкание с захватом подобно объекту-функтору с подходящими и инициализированными частными объектами-членами.
Ответ 3
Возможно, лучше подумать о лямбда-выражении как о объекте, который имеет оператор ()
, а не только функцию. Объект лямбда может иметь поля, которые запоминают (или "захватывают" ) переменные из лямбды во время построения лямбда, , которые будут использоваться позже во время выполнения лямбда.
Список захвата - это просто объявление таких полей.
(Вам даже не нужно указывать список захвата самостоятельно - синтаксис [&]
или [=]
инструктирует компилятор автоматически определять список захвата, основываясь на том, какие переменные из внешней области используются в лямбда-теле.)
Нормальная функция не может содержать состояние - она не может "помнить" аргументы за один раз для использования в другом. Лямбда может. Класс, созданный вручную, с пользовательским оператором ()
(он же "функтор" ) также может, но гораздо менее удобен синтаксически.
Ответ 4
Рассмотрим это:
std::function<int()>
make_generator(int const& i)
{
return [i] { return i; };
}
// could be used like so:
int i = 42;
auto g0 = make_generator(i);
i = 121;
auto g1 = make_generator(i);
i = 0;
assert( g0() == 42 );
assert( g1() == 121 );
Обратите внимание, что в этой ситуации два генератора, которые были созданы, имеют свой собственный i
. Это не то, что вы можете воссоздать с помощью обычных функций, и поэтому они не используют списки захвата. Списки захвата решают одну версию проблемы funarg.
Являются ли лямбда-функции более чем безымянными функциями?
Это очень умный вопрос. Созданные лямбда-выражения на самом деле более мощные, чем обычные функции: они замыкания (и стандарт действительно относится к объектам, которые лямбда-выражения создают как "объекты закрытия" ). Короче говоря, замыкание - это функция, объединенная со связанной областью. Синтаксис С++ решил представлять бит функции в знакомой форме (список аргументов с задержкой типа возврата с телом функции, некоторые части необязательны), в то время как список захвата является синтаксисом, который указывает, какая локальная переменная будет участвовать в связанной области (нелокальная переменные автоматически вводятся).
Обратите внимание, что на других языках с закрытием обычно нет конструкции, аналогичной спискам захвата С++. С++ сделал выбор дизайна списков захвата из-за своей модели памяти (локальная переменная живет только до тех пор, пока локальная область) и ее философия не платит за то, что вы не используете (локальные переменные теперь автоматически проживают дольше, если они "захваченное может быть нежелательным в каждом случае).
Ответ 5
Являются ли лямбда-функции более чем безымянными функциями?
ДА! Помимо безымянности, они могут ссылаться на переменные в лексически закрытой области. В вашем случае примером будет переменная sum_of_elems
(ее ни параметр, ни глобальная переменная, я полагаю). Нормальные функции в С++ не могут этого сделать.
В какой дополнительной информации содержится список захвата?
Список захвата обеспечивает
- список переменных, которые нужно записать с помощью
- информация о том, как они должны быть захвачены (по ссылке/по значению)
В других (например, функциональных) языках это необязательно, потому что они всегда ссылаются на значения одним способом (например, если значения неизменяемы, захват будет по значению, другая возможность состоит в том, что все является переменной на куче, поэтому все захватывается рефренсом, и вам не нужно беспокоиться о его жизни и т.д.). В С++ вы должны указать его для выбора между ссылкой (можно изменить переменную снаружи, но взорвется, когда лямбда переживет переменную) и значение (все изменения изолированы внутри лямбды, но будут жить до тех пор, пока лямбда - в основном, это будет поле в структуре, представляющей лямбда).
Вы можете заставить компилятор захватить все необходимые переменные, используя символ захвата по умолчанию, который просто указывает режим захвата по умолчанию (в вашем случае: &
= > ссылка, =
будет значением). В этом случае в основном фиксируются все переменные, указанные в лямбда из внешней области.