В синтаксисе лямбда-функций, для чего служит «список захвата»? - PullRequest
21 голосов
/ 04 марта 2012

Взято из ответа на этот вопрос , например, это код, который вычисляет сумму элементов в std::vector:

std::for_each(
    vector.begin(),
    vector.end(),
    [&](int n) {
        sum_of_elems += n;
    }
);

I понимаю, что лямбда-функции - это просто безымянные функции.

Я понимаю синтаксис лямбда-функций , как объяснено здесь .

Iне понимаю почему лямбда-функции нуждаются в списке захвата, а обычные функции - нет.

  1. Какую дополнительную информацию предоставляет список захвата?
  2. Почему обычные функции не нужныэта информация?
  3. Являются ли лямбда-функции больше, чем просто безымянные функции?

Ответы [ 5 ]

26 голосов
/ 04 марта 2012

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

Обычные функции могут использовать внешние данные несколькими способами:

  1. Статические поля
  2. Поля экземпляра
  3. Параметры (включая эталонные параметры)
  4. Глобал

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

Как говорится в этом ответе, вы также можете указать способ съемки. awoodland дает несколько примеров в другой ответ . Например, вы можете захватить одну внешнюю переменную по ссылке (например, по ссылочному параметру), а все остальные по значению:

[=, &epsilon]

EDIT:

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

Например, унарная функция принимает одно значение определенного типа и возвращает значение другого типа.

Однако внутренне он может использовать другие значения. В качестве тривиального примера:

[x, y](int z) -> int 
{
   return x + y - z;
}

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

19 голосов
/ 04 марта 2012

Основная проблема, которую мы пытаемся решить, заключается в том, что некоторый алгоритм ожидает функцию, которая принимает только определенный набор аргументов (один 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). Так, где мы помещаем другие данные?

Вы можете оставить другие данные в глобальной переменной или следовать традиционному подходу C ++ и написать функтор:

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; } };

Сокращенные списки захвата [=] и [&] просто захватывают «все» (соответственно по значению или по ссылке), что означает, что компилятор вычисляет конкретный список захвата для вас (он на самом деле не помещает все в объект замыкания, но только то, что вам нужно).

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

9 голосов
/ 04 марта 2012

Возможно, лучше думать о лямбда-выражении как о объекте , который имеет оператор (), а не просто функцию. Лямбда-«объект» может иметь поля, которые запоминают (или «захватывают») переменные вне лямбды во время создания лямбды, , которые будут использоваться позже во время выполнения лямбды.

Список захвата - это просто объявление таких полей.

(Вам даже не нужно указывать список захвата самостоятельно - синтаксис [&] или [=] указывает компилятору автоматически определять список захвата, основываясь на том, какие переменные из внешней области используются в лямбда-тело.)

Нормальная функция не может содержать состояние - она ​​не может «запоминать» аргументы за один раз для использования в другом. Лямбда может. Созданный вручную класс с пользовательским оператором () (он же "functor") также может, но синтаксически менее удобен.

3 голосов
/ 04 марта 2012

Учтите это:

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 .

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

Это очень умный вопрос. То, что создают лямбда-выражения, на самом деле более мощно, чем обычные функции: они замыкания (и Стандарт действительно ссылается на объекты, которые лямбда-выражения создают как «объекты замыкания»). Короче говоря, замыкание - это функция, объединенная с ограниченной областью действия. Синтаксис C ++ выбран для представления бита функции в привычной форме (список аргументов с отложенным типом возврата с телом функции, некоторые части необязательны), а список захвата - это синтаксис, который указывает, какая локальная переменная будет участвовать в связанной области (не локальной переменные вводятся автоматически).

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

1 голос
/ 04 марта 2012

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

ДА!Помимо того, что они безымянные, они могут ссылаться на переменные в лексически заключенной области видимости.В вашем случае примером будет переменная sum_of_elems (я полагаю, это не параметр и не глобальная переменная).Обычные функции в C ++ не могут этого сделать.

Какую дополнительную информацию предоставляет список захвата?

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

  1. список переменных для захвата с
  2. информацией о том, как их следует захватывать (по ссылке / по значению)

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

Вы можете настроить компилятор для захвата всех необходимых переменных, используя символ capture-default , который просто указывает значение по умолчаниюрежим захвата (в вашем случае: & => ссылка; = будет значение).В этом случае в основном все переменные, на которые ссылается лямбда из внешней области видимости, фиксируются.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...