Подтвердить что ты не робот

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

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

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

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

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

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

  • В какой дополнительной информации содержится список захвата?
  • Зачем нормальным функциям не нужна эта информация?
  • Являются ли лямбда-функции более чем безымянными функциями?
4b9b3361

Ответ 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 (ее ни параметр, ни глобальная переменная, я полагаю). Нормальные функции в С++ не могут этого сделать.

В какой дополнительной информации содержится список захвата?

Список захвата обеспечивает

  • список переменных, которые нужно записать с помощью
  • информация о том, как они должны быть захвачены (по ссылке/по значению)

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

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