之前写过一段时间的python,当时使用map和reduce,于是乎出现了很多这样的东西:
reduce(lambda x, y : x + y, range(100))
后来接触一点点js,因为看到了一个东西叫electron,后端是nodejs,前端是chromium,用web做页面,是原来atom的框架,后来vscode也是用它做的(因为这我还看了一丁丁vue)。所以还出现了这样的东西:
fs.open(file, (err, file) => {
// file operations})
讲道理我挺喜欢这种形状的lambda,比python的高到不知道哪里去,起码有个块了呗。后来知道了它的this绑定会和function(){ // operation } 不一样,但是也没有了解很多(以后有时间看看,感觉js写着蛮舒服)。
然后之前有上一个STL的选修课(超大教室上课,于是乎基本没听,期末作业也贼水),讲了remove_if这种东西,也用到了lambda:
std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9};
v.erase(std::remove_if(v.begin(), v.end(), [](const int &i) { return i % 2 == 1; }), v.end());
当我才看到这个写法的时候感到一阵疑惑:这个[]里面是个什么玩意儿,有时候写一个=有时候写一个&有时候又什么都不写,很迷。后来到现在才知道这个是捕获类型 。
那么什么是捕获类型呢。
首先要问问,这个匿名函数它究竟是个啥。
那么按照cppreference的说法,他是个闭包:cpprefernce中关于lambda表达式的解释
闭包在wiki上的解释:Inprogramming languages, a closure, also lexical closureor function closure, is a technique for implementing lexically scopedname binding in a language with first-class functions.
闭包是一种“在具有一等公民函数的语言中实现词法作用域名称绑定的技术”(渣翻译)。来看例子:
function wrapper() {
let str = "in wrapper"; // wrapper::str function inner() {
console.log(str); // wrapper::str可以被访问 }
inner();
}
wrapper();
执行结果打印"in wrapper"。
function wrapper() {
let str = "in wrapper"; // wrapper::str function inner() {
let str = "in inner"; // inner::str console.log(str); // inner::str 覆盖 wrapper::str }
inner();
}
wrapper();
执行结果打印"in inner"。
这就是词法作用域:变量属于的作用域是由变量声明的位置决定的,嵌套作用域可以访问外层作用域(如果没有被同名变量覆盖)。如果我们改动一下:
function wrapper() {
let str = "in wrapper"; // wrapper::str function inner() {
console.log(str);
}
return inner;
}
let f = wrapper(); // 离开wrapper作用域f();
执行结果打印"in wrapper" 。
在f()执行时,已经离开了wrapper的作用域,然而"in wrapper"还是被保留了下来。在这个例子中,wrapper()作用域中的局部变量str被绑定到f() 上,在离开了“词法作用域”之后仍然存在。这就是闭包。同时这个例子也展示了闭包的实现: 将上下文保存到函数对象里去。
c++的lambda中,这种保存(即捕获)的默认方式可以通过这个[]来指定:
struct lambda {
int i = 42;
auto f() {
return [=] { return i; }; // OK,以复制捕获lambda }
auto g() {
return [&] { return i; }; // OK,以引用捕获lambda }
// auto h() { // return [] { return i; }; // Error,不捕获lambda,访问不到lambda::i // }};
也可以为变量在默认捕获方式之外指定各自的捕获方式,形式上如同声明变量。引用捕获和复制捕获的区别也就如同字面意思,一个是将捕获的上下文引用过来,一个是复制一份,它们的区别相信写过这个的都知道了:
void swap_copy(int x, int y) { // 传值(复制) int temp = x;
x = y;
y = temp;
}
void swap_ref(int &x, int &y) { // 传引用 int temp = x;
x = y;
y = temp;
}
到现在,我们就知道了,lambda可以用来构建一个闭包,闭包就是一个绑定了词法作用域上下文的对象,当该作用域退出后,有了这个闭包,作用域中的上下文仍然可以访问——它的生存期看上去得以延长。
但是!
cpprefernce有一句话:若以引用隐式或显式捕获非引用实体,且在实体的生存期结束后调用闭包的函数调用运算符,则未定义行为发生。 C++ 闭包不通过被捕获的引用延长生存期
什么意思?
std::function f(int i) {
auto y = [=]() { return i; };
return y;
}
int main() {
auto p = f(3);
std::cout << p() << std::endl;
return 0;
}
结果是个3。在f()中,形参i被复制了一份到y里去,p是个包含被复制的f()的作用域上下文的闭包。如果我们改用引用捕获:
std::function f(int i) {
auto y = [&]() { return i; };
return y;
}
f()的作用域上下文被引用了一份给y,当f()退出以后,p保有了对于f()中i的引用。那么这个结果是啥呢?结果是不知道。 这就是“以引用显示捕获非引用实体,且在实体生存期结束后调用闭包的函数调用运算符”,是个UB。换句话说,这个i在f()调用结束之后就没了,随着f()的返回、调用栈弹出一并清除了。这样调用并不会出现编译错误,也可以运行,但是结果是不确定的,这种情况就不应该出现在代码中。
再次但是!
我在WSL下用g++编译,-std=c++11的情况下,结果就如同预测的一样,是个随机数;而改成-std=c++14之后:
#include
using std::cout;
using std::endl;
auto f(int i) {
auto y = [&] { return i; };
return y;
}
int main() {
auto p = f(3);
cout << p() << endl;
return 0;
}
结果是个3。而如果把返回类型改了:
#include
std::function f(int i) {
auto y = [&] { return i; };
return y;
}
就又是随机数了。
个中原因呢,我也不是很清楚。等一个评论区老哥给我解释一下。
当然,上面关于lambda的种种言论,有所谬误之处,也希望有老哥指出。具体见:Lambda 表达式 (C++11 起)zh.cppreference.com