Как работают статические переменные в объектах лямбда-функции? - PullRequest
50 голосов
/ 05 декабря 2011

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

Бесполезно Пример:

#include <iostream>
#include <vector>
#include <algorithm>

using std::cout;

void some_function()
{
    std::vector<int> v = {0,1,2,3,4,5};
    std::for_each( v.begin(), v.end(),
         [](const int &i)
         {
             static int calls_to_cout = 0;
             cout << "cout has been called " << calls_to_cout << " times.\n"
                  << "\tCurrent int: " << i << "\n";
             ++calls_to_cout;
         } );
}

int main()
{
    some_function();
    some_function();
}

Каков правильный вывод для этой программы?Это зависит от того, захватывает ли лямбда локальные переменные или нет?(это, безусловно, изменит базовую реализацию объекта функции, так что это может оказать влияние) Это допустимая поведенческая несогласованность?

Я не ищу: «Мой вывод компилятора ...», этоСлишком новая функция, чтобы доверять текущим реализациям ИМХО.Я знаю, что спрос на стандартные кавычки, кажется, популярен, поскольку мир обнаружил, что такая вещь существует, но все же я хотел бы найти достойный источник.

Ответы [ 5 ]

44 голосов
/ 06 декабря 2011

TL; Dr версия внизу.


§5.1.2 [expr.prim.lambda]

p1 лямбда-выражение :
лямбда-вводчик лямбда-декларатор опт составной оператор

p3 Тип лямбда-выражения (который также является типом объекта замыкания) - это уникальный, неназванный тип класса nonunion - так называемый тип замыкания - свойства которого описаны ниже. Этот тип класса не является совокупным (8.5.1). Тип замыкания объявляется в наименьшей области блока, области класса или области пространства имен, которая содержит соответствующее лямбда-выражение . ( Мое примечание: функции имеют область видимости блока. )

p5 тип закрытия для лямбда-выражения имеет открытый inline оператор вызова функции [...]

p7 Лямбда-выражение составное выражение дает тело функции (8.4) оператора вызова функции [...]

Поскольку составной оператор принимается непосредственно как тело оператора вызова функции, а тип замыкания определяется в наименьшей (самой внутренней) области видимости, это то же самое, что писать следующее:

void some_function()
{
    struct /*unnamed unique*/{
      inline void operator()(int const& i) const{
        static int calls_to_cout = 0;
        cout << "cout has been called " << calls_to_cout << " times.\n"
             << "\tCurrent int: " << i << "\n";
        ++calls_to_cout;

      }
    } lambda;
    std::vector<int> v = {0,1,2,3,4,5};
    std::for_each( v.begin(), v.end(), lambda);
}

Это допустимый C ++, функциям разрешено иметь static локальных переменных.

§3.7.1 [basic.stc.static]

p1 Все переменные, которые не имеют динамической длительности хранения, не имеют длительности хранения потоков и не являются локальными, имеют статическую длительность хранения. Хранение для этих объектов должно продолжаться в течение всей программы .

p3 Ключевое слово static может использоваться для объявления локальной переменной со статической продолжительностью хранения. [...]

§6.7 [stmt.dcl] p4
(Это касается инициализации переменных со статической продолжительностью хранения в области видимости блока.)

[...] В противном случае такая переменная инициализируется при первом прохождении контроля через ее объявление; [...]


Повторить:

  • Тип лямбда-выражения создается во внутренней области видимости.
  • Это , а не , создаваемое заново для каждого вызова функции (это не имеет смысла, поскольку тело включающей функции будет таким, как в моем примере выше).
  • Он подчиняется (почти) всем правилам обычных классов / структур (просто некоторые вещи о this отличаются), поскольку является типом класса, не являющимся объединением.

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

13 голосов
/ 06 декабря 2011

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

В дальнейшем, calls_to_cout захватывается значением, что дает лямбда-переменную-член с таким же именем, инициализированной текущим значением calls_to_cout. Эта переменная-член сохраняет свое значение при вызовах, но является локальной для лямбда-объекта, поэтому любые копии лямбды получат свою собственную переменную-члену call_to_cout вместо всех, совместно использующих одну статическую переменную. Это намного безопаснее и лучше.

(и поскольку лямбда-выражения по умолчанию являются константными, и эта лямбда-код модифицирует calls_to_cout, она должна быть объявлена ​​как изменяемая.)

void some_function()
{
    vector<int> v = {0,1,2,3,4,5};
    int calls_to_cout = 0;
    for_each(v.begin(), v.end(),[calls_to_cout](const int &i) mutable
    {
        cout << "cout has been called " << calls_to_cout << " times.\n"
          << "\tCurrent int: " << i << "\n";
        ++calls_to_cout;
    });
}

Если вы do хотите, чтобы одна и та же переменная была разделена между экземплярами лямбды, вам все же лучше использовать захваты. Просто запишите какую-то ссылку на переменную. Например, вот функция, которая возвращает пару функций, которые имеют ссылку на одну переменную, и каждая функция выполняет свою собственную операцию над этой общей переменной при вызове.

std::tuple<std::function<int()>,std::function<void()>>
make_incr_reset_pair() {
    std::shared_ptr<int> i = std::make_shared<int>(0);
    return std::make_tuple(
      [=]() { return ++*i; },
      [=]() { *i = 0; });
}

int main() {
    std::function<int()> increment;
    std::function<void()> reset;
    std::tie(increment,reset) = make_incr_reset_pair();

    std::cout << increment() << '\n';
    std::cout << increment() << '\n';
    std::cout << increment() << '\n';
    reset();
    std::cout << increment() << '\n';
6 голосов
/ 27 января 2016

Статическое может быть построено в захвате: -

auto v = vector<int>(99);
generate(v.begin(), v.end(), [x = int(1)] () mutable { return x++; });

Лямбда-баночка, изготовленная другой лямбдой

auto inc = [y=int(1)] () mutable { 
    ++y; // has to be separate, it doesn't like ++y inside the []
    return [y, x = int(1)] () mutable { return y+x++; }; 
};
generate(v.begin(), v.end(), inc());

Здесь y также может быть захвачен по ссылке, пока inc длится дольше.

2 голосов
/ 06 декабря 2011

У меня нет копии окончательного стандарта, и черновик , по-видимому, не решает проблему в явном виде (см. Раздел 5.1.2, начиная со страницы 87 в PDF). Но это говорит о том, что лямбда-выражение оценивает один объект типа закрытия , который может быть вызван повторно. При этом я считаю, что стандарт требует, чтобы статические переменные инициализировались один раз и только один раз, как если бы вы выписали класс operator() и захват переменных вручную.

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

0 голосов
/ 07 декабря 2015

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

В этом случае, даже если лямбда-объектвозвращается дважды, значения сохраняются:

auto make_sum()
{
    static int sum = 0;
    static int count = 0;

    //Wrong, since these do not have static duration, they are implicitly captured
    //return [&sum, &count](const int&i){
    return [](const int&i){
        sum += i;
        ++count;

        cout << "sum: "<< sum << " count: " << count << endl;
    };
}

int main(int argc, const char * argv[]) {
    vector<int> v = {0,1,1,2,3,5,8,13};

    for_each(v.begin(), v.end(), make_sum());

    for_each(v.begin(), v.end(), make_sum());

    return 0;
}

против:

auto make_sum()
{
    return [](const int&i){
        //Now they are inside the lambda
        static int sum = 0;
        static int count = 0;

        sum += i;
        ++count;

        cout << "sum: "<< sum << " count: " << count << endl;
    };
}

int main(int argc, const char * argv[]) {
    vector<int> v = {0,1,1,2,3,5,8,13};

    for_each(v.begin(), v.end(), make_sum());

    for_each(v.begin(), v.end(), make_sum());

    return 0;
}

Оба дают одинаковый вывод:

sum: 0 count: 1
sum: 1 count: 2
sum: 2 count: 3
sum: 4 count: 4
sum: 7 count: 5
sum: 12 count: 6
sum: 20 count: 7
sum: 33 count: 8
sum: 33 count: 9
sum: 34 count: 10
sum: 35 count: 11
sum: 37 count: 12
sum: 40 count: 13
sum: 45 count: 14
sum: 53 count: 15
sum: 66 count: 16
...