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

С++ 11 лямбда, возвращающая лямбда

этот фрагмент кода не является чем-то неизвестным разработчикам JS

function get_counter()
{
    return (
        function() {
            var c = 0;
            return function() { return ++c; };
        })();
}

он в основном создает a, который создает разные счетчики. Поэтому мне было интересно, можно ли сделать то же самое в С++ 11 с новой семантикой лямбда? Я закончил писать этот кусок С++, который, к сожалению, не компилируется!

int main()
{
    int c;
    auto a = [](){
        int c = 0;
        return [&](){
            cout << c++;
        };
    };
    return 0;
}

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

Кстати, я использую компилятор VS2012 и генерирует эту ошибку:

Error   2   error C2440: 'return' : cannot convert from 'main::<lambda_10d109c73135f5c106ecbfa8ff6f4b6b>::()::<lambda_019decbc8d6cd29488ffec96883efe2a>' to 'void (__cdecl *)(void)'    c:\users\ali\documents\visual studio 2012\projects\test\test\main.cpp   25  1   Test
4b9b3361

Ответ 1

В вашем коде есть ошибка, так как в нем содержится оборванная ссылка; ссылка c будет ссылаться на локальную переменную во внешней лямбда, которая будет уничтожена при возврате внешней лямбда.

Вы должны написать его, используя mutable байпасный захват lambda:

auto a = []() {
    int c = 0;
    return [=]() mutable {
        cout << c++;
    };
};

Это зависит от постстандартного расширения, позволяющего использовать несколько операторов в lambda с возвратным типом; Есть ли причина, по которой lambdas не позволяет выводить возвращаемый тип, если он содержит более одного оператора? Самый простой способ исправить это - указать параметр, чтобы лямбда содержит только один оператор:

auto a = [](int c) {
    return [=]() mutable {
        cout << c++;
    };
};

К сожалению, параметры по умолчанию не разрешены в lambdas, поэтому вы должны называть это как a(0). В качестве альтернативы за счет удобства чтения вы можете использовать вложенный лямбда-вызов:

auto a = []() {
    return ([](int c) {
        return [=]() mutable {
            cout << c++;
        };
    })(0);
};

Как это работает, когда a выполняет внутреннюю лямбду копирует все ссылочные переменные в экземпляр своего типа закрытия, что здесь будет примерно так:

struct inner_lambda {
    int c;
    void operator()() { cout << c++; }
};

Затем экземпляр типа замыкания возвращается внешней лямбдой и может быть вызван и будет изменять свою копию c при вызове.

В целом, ваш (фиксированный) код переводится на:

struct outer_lambda {
    // no closure
    struct inner_lambda {
        int c;    // by-value capture
        // non-const because "mutable"
        void operator()() { cout << c++; }
    }
    // const because non-"mutable"
    inner_lambda operator()(int c) const {
        return inner_lambda{c};
    }
};

Если вы оставили c как захват по ссылке, это будет:

struct outer_lambda {
    // no closure
    struct inner_lambda {
        int &c;    // by-reference capture
        void operator()() const { cout << c++; } // const, but can modify c
    }
    inner_lambda operator()(int c) const {
        return inner_lambda{c};
    }
};

Здесь inner_lambda::c является оборванной ссылкой на переменную локального параметра c.

Ответ 2

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

Я думаю, что вам нужен код:

return [=]() mutable {
    cout << c++;
};

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

Поэтому каждый раз, когда вы вызываете a, вы получаете другой счетчик со своим собственным счетом, начинающимся с 0. Каждый раз, когда вы вызываете этот счетчик, он увеличивает свою собственную копию c. Насколько я понимаю Javascript (недалеко), это то, что вы хотите.

Ответ 3

Я думаю, проблема заключается в том, что компилятор не может выводить возвращаемый тип внешней лямбды (которая присваивается a), потому что она состоит из более простого возврата одной строки. Но, к сожалению, также нет способа явно указать тип внутренней лямбды. Поэтому вам нужно будет вернуть std::function, который будет содержать дополнительные накладные расходы:

int main()
{
    int c;
    auto a = []() -> std::function<void()> {
        int c = 0;
        return [=]() mutable {
            std::cout << c++;
        };
    };
    return 0;
}

И, конечно же, вы должны уловить по значению, как Стив уже объяснил в своем ответе.

РЕДАКТИРОВАТЬ: Что касается точной ошибки, то она не может преобразовать возвращаемую внутреннюю лямбда в void(*)() (указатель на функцию void()), у меня есть только некоторые догадки, потому что я не имеют много понимания их лямбда-реализации, что я не уверен, что это стабильный или стандартно-совместимый вообще.

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

РЕДАКТИРОВАТЬ: И, как и выражения ecatmur в его комментарии, возвращение std::function даже необходимо при создании фактической функции get_counter (вместо лямбда), поскольку нормальные функции не имеют любой автоматический вывод типа возврата.

Ответ 4

Первое, что вы должны знать, это то, что даже если вы получите синтаксис для компиляции, семантика отличается. В С++ lambdas, которые захватывают путем ссылки, только обычную ссылку, которая не продлит время жизни объекта, связанного этой ссылкой. То есть время жизни c связано со временем жизни охватывающей лямбда:

int main()
{
    int c;
    auto a = [](){
        int c = 0;
        return [&](){
            return ++c;
        };
    }();                     // Note: added () as in the JS case
    std::cout << a() << a();
    return 0;
}

После добавления отсутствующего (), чтобы внешняя лямбда была оценена, ваша проблема заключается в том, что c, который удерживается ссылкой в ​​возвращенной лямбда, уже недействителен после оценки полного выражения.

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

int main()
{
    int c;
    auto a = [](){
        std::shared_ptr<int> c = std::make_shared<int>(0);
        return [=](){
            return ++(*c);
        };
    }();                     // Note: added () as in the JS case
    std::cout << a() << a();
    return 0;
}

Это должно скомпилироваться и работать должным образом. Всякий раз, когда внутренняя лямбда освобождается (a выходит за рамки), счетчик будет освобожден из памяти.

Ответ 5

Это работает на g++ 4.7

#include <iostream>
#include <functional>                                                                           

std::function<int()> make_counter() {
    return []()->std::function<int()> {
        int c=0;
        return [=]() mutable ->int {
            return  c++ ;
        };  
    }();
}   


int main(int argc, char * argv[]) {
    int i = 1;
    auto count1= make_counter();
    auto count2= make_counter();

    std::cout << "count1=" << count1() << std::endl;
    std::cout << "count1=" << count1() << std::endl;
    std::cout << "count2=" << count2() << std::endl;
    std::cout << "count1=" << count1() << std::endl;
    std::cout << "count2=" << count2() << std::endl;
    return 0;
}
Valgrind вообще не жалуется на это. Каждый раз, когда я вызываю make_counter, valgrind сообщает о дополнительном распределении и бесплатном, поэтому я предполагаю, что код метапрограмм лямбда вставляет код выделения для памяти для переменной c (думаю, я могу проверить отладчик). Интересно, соответствует ли это Cxx11 или просто g++. Clang 3.0 не будет компилировать это, потому что у него нет функции std:: (возможно, я могу попробовать использовать функцию boost).