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

Как работают статические переменные в лямбда-функциях?

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

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

#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();
}

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

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

4b9b3361

Ответ 1

tl; dr версия внизу.


§5.1.2 [expr.prim.lambda]

p1 лямбда-выражение:
lambda-entercer lambda-declarator opt составной оператор

p3 Тип лямбда-выражения (который также является типом объекта замыкания) - это уникальный, неназванный тип невозвратного типа, называемый типом замыкания, свойства которого описаны ниже. Этот тип класса не является совокупностью (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);
}

Что является законным С++, функциям разрешено иметь локальные переменные static.

§3.7.1 [basic.stc.static]

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

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

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

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


Повторить:

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

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

Ответ 2

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

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

(и поскольку lambda const по умолчанию и эта лямбда модифицирует 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;
    });
}

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

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

Ответ 3

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

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

Ответ 4

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

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 длится дольше.

Ответ 5

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

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

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