Ric*_*ick 2 c++ lambda closures
我一直很困惑什么是 C++ 中的闭包。我读过这篇什么是“关闭”?但几乎所有答案都指的是 JavaScript,但我认为 C++ 和 JavaScript 之间的闭包存在一些差异。所以我发现很难将闭包的 JavaScript 描述与 C++ 相匹配。
例如,几乎所有答案都以函数返回函数为例来演示 JavaScript 中的闭包。但是我在 C++ 中没有找到类似的模式。
更重要的是,在 JavaScript 中没有所谓的“捕获列表”这样的东西。
示例 1:
int a = 3;
int am_I_a_closure(int c){
return c + a;
}
int main(){
}
Run Code Online (Sandbox Code Playgroud)
我的意思是,通过正常的名称查找过程,如果在当前作用域中找不到名称,则在外部作用域中找到它,然后在更多外部作用域中找到它...
为什么需要捕获列表?为什么需要捕获外部作用域变量?不能通过正常的名称查找来完成吗?
示例 2:
int main(){
int a = 3;
{
int b = 5;
{
int c = 4;
{
std::cout << a+b+c <<std::endl;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
示例 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.
}
Run Code Online (Sandbox Code Playgroud)
同样,在示例 3 中,为什么a需要捕获而不是示例 1 和示例 2 中的正常名称查找?
重要的是要理解“闭包”是一个在函数式编程中具有非常特殊含义的概念。然而,C++ 不是一种函数式语言;它不太关心严格遵守函数式编程术语。它只是定义了各种功能,其中一些可能会也可能不会很好地映射到该术语。
JavaScript 和 C++ 是不同的语言。在 JavaScript 中,函数有一个属性,称为“一等对象”。这意味着,当您执行代码以创建“函数”时,您正在创建一个代表该函数的对象。包含函数的变量与包含字符串的变量或包含数组的变量或其他任何东西从根本上没有区别. 您可以用数组覆盖包含函数的变量,反之亦然。
特别是,作为一流对象的函数可以在创建时具有与其相关联的状态。如果这样的函数超出其范围访问局部变量,则该范围可以作为函数状态的一部分存储;当您尝试在函数中使用该变量时,将自动访问此状态。因此,您似乎“超出”了函数的范围,但实际上并没有;范围是随您“带入”的,您只是在访问它。
在 C++ 中,函数不是一等对象。您可以获得指向函数的指针,但函数指针与对象指针明显不同(甚至不需要在两者之间进行转换才能有效)。就 C++ 语言而言,函数不是“创建”或“销毁”的;从程序开始到结束,每个函数都在那里。
C++ 函数可以访问全局变量,但那是因为它们是global。全局变量的位置在编译/链接时被烘焙到可执行文件中,因此无需将特殊状态与函数一起存储以访问它。
但是,C++ 确实有一个有用的概念,可以帮助创建一流函数对象的效果。即,类类型可以重载函数调用 operator operator()。这允许类的实例被调用,就好像它是一个函数一样。类实例是对象,可以有内部状态(又名:成员变量),operator()重载只是该类型的成员函数。
鉴于所有这些,您可以创建一些模拟适当范围的函数对象的东西。您所需要的只是一个具有成员变量的类,这些成员变量对应于它引用的函数范围之外的变量。通过将外部值传递给构造函数,可以在类的构造函数中初始化这些成员。然后你就有了一个可以调用的有效对象,它可以通过使用它的成员变量来访问那些“外部”变量。
这就是 C++ lambda 的全部内容。它将所有这些都包装在“漂亮、整洁”的语法中。它为你写了一个类;它为您编写从外部世界“捕获”的成员变量,并调用构造函数并为您传递这些变量。
然而,C++ 是一种努力不让某些东西变得比你需要的更昂贵的语言。您使用的外部变量越多,lambda 需要的内部成员变量就越多,因此类越大,初始化/复制等所需的时间就越长。因此,如果你想使用一些外部变量(它被实现为成员变量),C++ 要求你要么显式地列出它(这样你就知道你打算捕获它)或使用默认的捕获机制[=]或[&](所以您明确放弃了抱怨不小心使您的 lambda 类型变大和/或变慢的权利)。
此外,在 JavaScript 中,一切都是引用。变量存储对数组、函数、字典等的引用。JavaScript 是一种基于引用的语言。
C++ 是一种面向价值的语言。JavaScript 中的变量引用一个对象;C++ 中的变量是一个对象。在 C++ 中不能用另一个对象替换一个对象;你可以复制一个对象的值,但它仍然是那个对象。
因此,lambda 应该如何捕获特定变量变得相关。您可以通过值(将值复制到隐藏成员中)或通过引用(引用对象)来捕获变量。
这一点特别重要,因为 C++ 不会被垃圾收集。这意味着,仅仅因为您引用了一个对象并不意味着该对象仍然存在。如果您在堆栈上有一个变量,并且您获得了对它的引用,并且该引用存在于堆栈变量超出范围的点之后……该引用现在无用。在 JavaScript 中,由于垃圾收集,它会很好。但是 C++ 不会这样做。您有一个无法使用的已销毁对象的引用。
因此,如果您希望 lambda 捕获局部变量,并且希望 lambda 持续超过变量不再存在的点,则需要按值而非引用来捕获此类变量。
通过值或引用捕获由以下因素决定如何你列出捕获列表中的变量。&x表示按引用捕获,而x按值捕获。默认捕获[=]是默认按值[&]捕获,默认是引用捕获。