C++ lambda函数的词法闭包和局部变量

6

简介

在C++中,当我从一个函数返回一个捕获了该函数的局部变量的lambda时,具体会发生什么以及为什么?编译器(g ++)似乎允许它,但它给了我不同于我预期的结果,所以我不确定这是否在技术上是安全/支持的。

细节

在一些语言(Swift,Lisp等)中,您可以在闭包/lambda中捕获局部变量,并且只要闭包在范围内,它们就保持有效并在范围内(我听说过在Lisp上下文中称之为“lambda over let over lambda”)。例如,在Swift中,我尝试做的示例代码是:

func counter(initial: Int) -> (() -> Int) {
    var count = initial
    return { count += 1; return count }
}

let c = counter(initial: 0)
c() // returns 1
c() // returns 2
c() // returns 3

我尝试编写了以下C++代码,与此相似:

auto counter(int initial)
{
    int count = initial;
    return [&count] () -> int {
        count = count + 1;
        return count;
    };
}

然而,我得到的结果是:
auto c = counter(0);
std::cout << c() << std::endl; // prints 1
std::cout << c() << std::endl; // prints 1
std::cout << c() << std::endl; // prints 1

如果我捕获了一个仍在作用域内的变量,它将按照我预期的方式工作。例如,如果我在单个函数中执行以下所有操作:

int count = 0;
auto c = [&count] () -> int {
    count = count + 1;
    return count;
};
std::cout << c() << std::endl; // prints 1
std::cout << c() << std::endl; // prints 2
std::cout << c() << std::endl; // prints 3

所以我猜我的问题是,在上面的第一个C++示例中,实际捕获了什么?这是定义行为还是只有对堆栈上某些随机内存的引用?


我不理解这行代码 auto c = counter();。你怎么可以在没有参数的情况下调用函数呢?似乎这甚至无法编译。 - Richard
@Richard,已修复,对此很抱歉。 - Chris Vig
在第一种情况下,您只是具有对堆栈上某些随机内存的引用。 - user1438832
2个回答

10
    return [&count] () -> int {

这是引用捕获。Lambda 捕获了对此对象的引用。

该对象是函数局部范围内的 count,因此当函数返回时,count 被销毁,这将成为一个引用到已经超出其作用域并被销毁的对象。使用这个引用会导致未定义行为。

通过值捕获似乎可以解决这个问题:

    return [count] () -> int {

但是你明显的意图是让每次调用此 lambda 函数返回单调递增的计数器值。仅仅通过按值捕获对象还不够。你还需要使用一个可变的 lambda 表达式

 return [count] () mutable -> int
 {
    return ++count;
 };

但是对于你的问题“发生了什么”,学究式的答案是 lambda 本质上是一个匿名类,lambda 捕获的实际上是类成员变量。你的 lambda 等效于:

然而,对于你的问题“会发生什么”,学究般的回答是 lambda 本质上是一个匿名类,而 lambda 捕获的则是类成员。你的 lambda 等价于:

class SomeAnonymousClassName {

     int &count;

public:
     SomeAnonymousClassName(int &count) : count(count)
     {}

     int operator()
     {
          // Whatever you stick in your lambda goes here.
     }
};

通过引用捕获某些内容会将其翻译为一个类成员,该成员是一个引用。通过值捕获某些内容将其翻译为一个非引用的类成员,并且捕获lambda变量的行为将它们传递给lambda类的构造函数,这就是创建lambda时发生的事情。实际上,lambda是匿名类的实例,具有定义的operator()

在常规lambda中,operator()实际上是一个带有const的操作符方法。在可变lambda中,operator()是一个非const的、可变的操作符方法。


抱歉,我不知道为什么我的编辑错误地删除了你的最后一段。 - Danh
啊,很酷。感谢清晰的解释和可变lambda的指针。 - Chris Vig

0
在第一种情况下,您正在捕获对本地变量的引用。该引用在函数返回后成为悬空引用。因此,您的程序可能会出现未定义的行为。

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