C++11 lambda 返回 lambda

26

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

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

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

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

我在想是否有一种解决方法可以将其编译,并且如果有的话,编译器如何使此代码正确运行?我的意思是它必须创建单独的枚举器,但还应该收集垃圾(未使用的变量)。

顺便说一下,我正在使用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

@ecatmur,我在这里更新并添加了我收到的错误消息,另外你有没有想法这段代码是否安全?我的意思是它是否浪费内存或者是否有一些隐藏的垃圾回收器实现? - Ali1S232
没有垃圾收集器,也不会浪费内存。每次调用a时,它都会返回一个新的lambda对象。你需要将a的返回值赋给某个变量 -- 当该变量被销毁时,lambda所使用的捕获变量的资源也会随之销毁。 - Steve Jessop
1
在C++11中,您需要一个形式为return expr;的主体来进行返回类型推断,但您没有它。如果没有来自C++11后期的扩展返回类型推断规则,此代码无论如何都不应编译。如果VS2012有这些规则,我会感到惊讶。 - Xeo
你的代码中有2个错误。一个是返回一个(可能)悬空引用的对象,另一个是没有指定适当的返回类型(从而导致编译器错误)。 - Walter
@Christian:在你回答之前我已经注意到了,但是我太懒了不想写一个,所以当你的回答出现时,我立刻点了个赞。:P - Xeo
显示剩余4条评论
6个回答

22

你的代码存在一个问题,即它包含一个悬空引用;c引用将指向外部lambda中的局部变量,在外部lambda返回时将被销毁。

你应该使用可变的按值lambda捕获来编写它:

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

这依赖于一个后标准扩展,允许在返回类型推导lambda中使用多个语句;为什么不允许包含多个语句的lambda推导返回类型? 解决方法最简单的方式是提供一个参数,以便lambda只包含一个语句:

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

遗憾的是,Lambda表达式中不允许默认参数,因此必须使用 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的悬空引用。


我给你点赞,引入该参数存在一定的风险,因为某些人可能会错误地指定它,因为设计上并没有这种能力。但实际上,这是对计数器生成器功能的很好扩展,所以将其添加到设计中并继续前进吧 :-) - Steve Jessop
1
@SteveJessop,结果证明在lambda函数中不允许使用默认参数(5.1.2:5)。有点烦人。 - ecatmur
是的,可能因为 lambda 表达式被设计成由 std::function 捕获,一旦您擦除了类型,就失去了默认参数的值。真可惜你不能在 擦除类型的情况下使用它们。我猜这并不被认为是一个主要的用例,但正如你所展示的,从函数返回 lambda 提供了一些实用的场景,其中返回可以带有可选参数的内容是很方便的。 - Steve Jessop
+1,你的“嵌套lambda调用”方法正是我所采用的。 - ildjarn

9

C++存在一个自然限制,即使用引用捕获的lambda表达式一旦所捕获的变量不存在了就不能再使用它。因此,即使你编译通过了,也不能从包含它的函数(可能是一个lambda表达式,但这与问题无关)返回这个lambda表达式,因为返回时自动变量 c 将被销毁。

我认为你需要的代码是:

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

我没有测试过它,也不知道哪些编译器版本支持它,但这是一种通过值捕获的方式,并使用 mutable 来表示 lambda 可以修改被捕获的值。

因此,每次调用 a 都会得到一个不同的计数器,其自己的计数从0开始。每次调用该计数器时,它会增加自己的 c 的副本。就我所了解的 JavaScript(不是很深入),这就是你想要的。


@Xeo:真的吗?因为没有尾随返回类型,而且lambda的主体不是return语句,所以它不是void吗?无论如何,如果有帮助的话,让它变成return [=]() mutable -> void。我认为这个cout << c++;只是为了调试,为了匹配Javascript,它将是return ++c;,这将允许返回类型推断(并从1开始计数,而不是0)。 - Steve Jessop
@SteveJessop 不是外部lambda表达式(指分配给a的那个)。请参阅我的答案。 - Christian Rau
@Christian:啊,我明白了。是的,要解决两个问题,我只解决了其中一个。 - Steve Jessop
1
5.1.2:22 [注意:如果一个实体被隐式或显式地引用捕获,那么在该实体的生命周期结束后调用相应lambda表达式的函数调用运算符可能导致未定义行为。—注解] 我没有看到lambda在那个点存在的任何问题。 - ecatmur
@ecatmur:既然这只是信息性的,那么“可能会”意味着它确实表现得像它持有一个悬空指针,并且函数“可能会”访问它所捕获的内容,导致未定义行为。所以,沃尔特说的没错。 - Steve Jessop
显示剩余5条评论

7

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

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

当然,你必须按值捕获,就像 Steve 在他的答案中已经解释的那样。
编辑:至于为什么确切的错误是它无法将返回的内部lambda转换为 void(*)()(指向 void()函数的指针),我只有一些猜测,因为我对他们的lambda实现没有太多了解,我不确定它是否稳定或符合标准。
但是,我认为VC至少尝试推断内部lambda的返回类型,并意识到它返回一个可调用对象。但是,它以某种方式错误地假设此内部lambda不会捕获任何内容(或者它们无法确定内部lambda的类型),因此它们只是使外部lambda返回一个简单的函数指针,如果内部lambda不捕获任何内容,这确实起作用。
编辑:正如ecatmur在他的评论中所述,在实际创建 get_counter 函数(而不是lambda)时返回 std :: function 甚至是必要的,因为普通函数没有任何自动返回类型推断。

1
这里有一个错误:默认情况下,按值捕获不允许修改被捕获的对象(隐式生成的 operator()const),这意味着内部 lambda 中的 c++ 将无法编译。请注意,外部 lambda 不需要是可变的。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas 你是指旧版本(外部lambda不是“mutable”)还是仍然存在错误?啊,别管我说的了。已经找到并纠正了,谢谢。 - Christian Rau
我可能错了,我对lambda没有太多经验,但是内部lambda通过值捕获然后尝试修改该值。如果我没记错的话,这要求内部lambda是可变的。 - David Rodríguez - dribeas
std::function是一个很好的建议,如果a是一个实际的函数,则是必要的。 - ecatmur

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后,您的问题是在完整表达式求值后,由返回的lambda引用保留的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;
}

那段代码应该可以编译并按预期工作。当内部的 lambda 被释放(a 超出了作用域)时,计数器将被从内存中释放。


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 function)。


0

我知道现在有点晚了,但是在C++14及以后的版本中,你现在可以初始化一个lambda捕获,从而使代码更加简单:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接