25

这段代码对于 JS 开发者来说并不陌生

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

它基本上创建了一个创建不同枚举数的。所以我想知道是否可以在 C++11 中使用新的 lambda 语义来完成同样的事情?我最终写了这段 C++ 不幸的是不能编译!

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
4

6 回答 6

22

您的代码有一个错误,它包含一个悬空引用;该c引用将引用外部 lambda 中的局部变量,该变量将在外部 lambda 返回时被销毁。

您应该使用mutable按值 lambda 捕获来编写它:

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

这依赖于后标准扩展,以允许在返回类型推导 lambda 中使用多个语句;如果它包含多个语句,是否有理由不允许 lambdas 推断返回类型?修复它的最简单方法是提供一个参数,以便 lambda 仅包含一条语句:

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

不幸的是,lambdas 中不允许使用默认参数,因此您必须将其称为a(0). 或者以可读性为代价,您可以使用嵌套的 lambda 调用:

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

它的工作方式是,当a执行内部 lambda 时,将所有引用的变量复制到其闭包类型的实例中,这将类似于:

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

然后闭包类型的实例由外部 lambda 返回,并且可以被调用并在调用时修改其副本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

于 2012-09-28T12:14:08.013 回答
9

一旦变量不再存在,通过引用捕获的 lambda 不能再使用捕获的变量,这是 C++ 的一个自然限制。所以即使你让它编译,你也不能从它出现的函数中返回这个 lambda(这也恰好是一个 lambda,但这无关紧要),因为自动变量c在返回时被破坏。

我认为您需要的代码是:

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

我没有测试过,也不知道哪些编译器版本支持它,但这是按值捕获,mutable也就是说捕获的值可以由 lambda 修改。

因此,每次调用时,a您都会得到一个不同的计数器,其计数从 0 开始。每次调用该计数器时,它都会增加自己的c. 据我了解Javascript(不远),这就是你想要的。

于 2012-09-28T12:11:03.817 回答
7

我认为问题在于编译器无法推断出外部 lambda 的返回类型(分配给a),因为它包含的不仅仅是一个简单的单行返回。但不幸的是,也没有办法明确说明内部 lambda 的类型。因此,您将不得不返回 a std::function,这会带来一些额外的开销:

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

当然,您必须按价值捕获,就像史蒂夫在他的回答中已经解释的那样。

编辑:至于为什么确切的错误是它不能将返回的内部 lambda 转换为void(*)()(指向void()函数的指针),我只有一些猜测,因为我对他们的 lambda 实现没有太多了解,我不确定是不是完全稳定或符合标准。

但我认为 VC 至少尝试推断出内部 lambda 的返回类型并意识到它返回一个可调用对象。但是后来它以某种方式错误地假设这个内部 lambda 没有捕获(或者他们无法确定内部 lambda 的类型),所以他们只是让外部 lambda 返回一个简单的函数指针,如果内部 lambda 不会,这确实可以工作捕捉任何东西。

编辑:就像ecatmur在他的评论中所说的那样,std::function在制作实际get_counter函数(而不是 lambda)时甚至需要返回 a ,因为普通函数没有任何自动返回类型推导。

于 2012-09-28T12:19:43.220 回答
3

您应该知道的第一件事是,即使您获得了要编译的语法,语义也是不同的。在按引用捕获的 C++ lambda 中,仅捕获一个普通引用,这不会延长该引用绑定的对象的生命周期。也就是说,生命周期c绑定到封闭 lambda 的生命周期:

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

添加缺失()以便评估外部 lambda 后,您的问题是c返回的 lambda 中通过引用保存的 lambda 在评估完整表达式后不再有效。

话虽如此,以额外的动态分配为代价(这相当于 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;
}

应该可以按预期编译和工作。每当内部 lambda 被释放(a超出范围)时,计数器将从内存中释放。

于 2012-09-28T12:18:23.467 回答
2

这适用于 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 都会报告一个额外的分配和空闲,所以我假设 lambda 元编程代码正在为变量 c 插入内存​​分配代码(我想我可以检查调试器)。我想知道这是否符合 Cxx11 或只是 g++ 特定的。Clang 3.0 不会编译它,因为它没有 std::function (也许我可以尝试使用 boost 函数)。

于 2013-02-23T18:05:21.247 回答
0

我知道这已经很晚了,但是在 C++14 及更高版本中,您现在可以初始化 lambda 捕获,从而获得更简单的代码:

auto a = []() {
    return [c=0]() mutable {
        cout << c++;
    };
};
于 2020-04-14T20:57:06.703 回答