如果全局函数使用非局部变量,那么它是一个闭包,这是否正确?

Is it correct that if a global function uses non-local variables, then it's a closure?

本文关键字:一个 闭包 是否 函数 全局 局部变量 如果      更新时间:2024-05-10

我一直很困惑C++中的闭包是什么。我读过这个什么是';闭包';?但几乎所有的答案都涉及JavaScript,但我认为C++和JavaScript在闭包方面存在一些差异。因此,我发现很难将闭包的JavaScript描述与C++相匹配。

例如,几乎所有的答案都是以返回函数的函数为例来演示JavaScript中的闭包。但我在C++中找不到类似的模式。

更重要的是,在JavaScript中没有所谓的"捕获列表"。


  1. 有人告诉我,如果一个函数使用非局部变量(来自外部范围或全局范围),那么它就是一个闭包。这是正确的吗

示例1:

int a = 3;
int am_I_a_closure(int c){
return c + a;
}
int main(){
}
  1. 为什么需要捕获列表?C++中的lambda不能像JavaScript嵌套函数一样工作吗?或者换句话说,C++中的lambda不能像全局函数访问全局(非局部)变量一样工作吗

我的意思是,通过正常的名称查找过程,如果在当前作用域中找不到名称,那么在外部作用域中找到它,然后在更多的外部作用域。。。

为什么需要捕获列表?为什么需要捕获外部范围变量?难道不能通过正常的名字查询来完成吗?

示例2:

int main(){
int a = 3;
{
int b = 5;
{
int c = 4;
{
std::cout << a+b+c <<std::endl;
}
}
}
}

示例3:

int main(){
std::vector<int> values = {1,5,3,4,3};
int a = 3;
std::find_if(values.begin(), values.end(), [](int value) {return value > a; }); //Error, `a` is not captured.
}

同样,在示例3中,为什么需要捕获a而不是像示例1和示例2中那样的正常名称查找?

重要的是要理解"闭包"是一个在函数式编程中具有非常特殊意义的概念。然而,C++不是一种函数式语言;它并不太关心严格遵守函数式编程术语。它简单地定义了各种功能,其中一些功能可能很好地映射到该术语上,也可能不映射到该词汇上。

JavaScript和C++是不同的语言。在JavaScript中,函数有一个称为"一流对象"的属性。这意味着,当您执行代码来创建"函数"时,您正在创建一个表示该函数的对象。包含函数的变量与包含字符串的变量或包含数组的变量或其他任何变量基本上没有什么不同。您可以用数组覆盖包含函数的变量,反之亦然。

特别是,作为一级对象的函数在创建时可以具有与其相关联的状态。如果这样的函数超出其范围访问局部变量,那么该范围可以作为函数状态的一部分存储;当您尝试在函数中使用该变量时,将自动访问该状态。因此,看起来你正在"超出"函数的范围,但你并没有;作用域是随你"进来"的,你只是在访问它。

在C++中,函数不是一类对象。您可以获得指向函数的指针,但函数指针与对象指针显式不同(甚至不要求在两者之间进行强制转换)。就C++语言而言,函数不是"创建"或"销毁"的;从程序开始到结束,每个函数都在那里。

C++函数可以访问全局变量,但这是因为它们是全局。全局变量的位置在编译/链接时被烘焙到可执行文件中,因此不需要与函数一起存储特殊状态即可访问它

然而,C++确实有一个有用的概念,可以帮助创建一流函数对象的效果。也就是说,类类型可以重载函数调用运算符operator()。这样就可以像调用函数一样调用类的实例。类实例是对象,可以具有内部状态(也称为成员变量),operator()重载只是该类型的成员函数。

考虑到所有这些,您可以创建一些模拟适当范围的函数对象的东西。您所需要的只是一个具有成员变量的类,这些变量对应于它引用的函数范围之外的变量。通过将外部值传递给构造函数,可以在类的构造函数中初始化这些成员。然后,您有一个可以调用的有效对象,它可以通过使用其成员变量来访问这些"外部"变量。

这就是C++lambda的全部。它用"漂亮、整洁"的语法将所有这些都封装起来。它为你写一节课;它为您编写从外部世界"捕获"的成员变量,并调用构造函数并为您传递这些变量。

然而,C++是一种努力不让某些东西变得比你需要的更昂贵的语言。你使用的外部变量越多,lambda需要的内部成员变量就越多,因此类就越大,初始化/copy等所需的时间就越长。因此,如果您想使用某个外部变量(作为成员变量实现),C++要求您要么显式列出它(这样您就知道您想要捕获它),要么使用默认捕获机制[=][&](这样您就会显式放弃抱怨意外使lambda类型变大和/或变慢的权利)。

此外,在JavaScript中,所有内容都是引用。变量存储对数组、函数、字典等的引用。JavaScript是一种基于引用的语言。

C++是一种面向值的语言。JavaScript中的变量引用一个对象;C++中的变量是一个对象。在C++中,不能用另一个对象替换一个对象;您可以复制一个对象的值,但它仍然是那个对象。

因此,lambda应该如何捕获特定的变量就变得相关了。可以通过值(将值复制到隐藏成员中)或通过引用(引用对象)捕获变量。

这一点特别重要,因为C++不是垃圾收集的。这意味着,仅仅因为你引用了一个对象,并不意味着对象仍然存在。如果堆栈上有一个变量,并且您获得了对它的引用,并且该引用存在于堆栈变量超出范围的点之后。。。这种提法现在毫无用处。在JavaScript中,由于垃圾收集,它会很好。但是C++不能做到这一点。您有一个对已销毁对象的引用,该对象无法使用。

因此,如果您希望lambda捕获局部变量,并且希望lambda保持到变量不再存在的点之后,则需要通过捕获此类变量,而不是通过引用。

通过值或引用捕获由如何在捕获列表中列出变量决定。&x表示通过引用捕获,而x表示通过值捕获。默认捕获[=]表示默认按值捕获,[&]表示默认参考捕获。