关键词:闭包;
总结:
形式 auto f1 = [](){}; // 相当于空函数,什么也不做
- 可以匿名使用
vector<int> v = {3, 1, 8, 5, 0}; // 标准容器
cout << *find_if(begin(v), end(v), // 标准库里的查找算法
[](int x) // 匿名lambda表达式,不需要auto赋值
{
return x >= 5; // 用做算法的谓词判断条件
} // lambda表达式结束
)
<< endl; // 语句执行完,lambda表达式就不存在了
- 如何捕获外部变量
- lambda 表达式是一个闭包,能够像函数一样被调用,像变量一样被传递;
- 可以使用 auto 自动推导类型存储 lambda 表达式,但 C++ 鼓励尽量就地匿名使用,缩小作用域;
- lambda 表达式使用“[=]”的方式按值捕获,使用“[&]”的方式按引用捕获,空的“[]”则是无捕获(也就相当于普通函数);
- 捕获引用时必须要注意外部变量的生命周期,防止变量失效;C++14 里可以使用泛型的 lambda 表达式,相当于简化的模板函数。
函数在用法上也有一些特殊之处。在 C/C++ 里,所有的函数都是全局的,没有生存周期的概念(static、名字空间的作用很弱,只是简单限制了应用范围,避免名字冲突)。而且函数也都是平级的,不能在函数里再定义函数,也就是不允许定义嵌套函数、函数套函数。
认识 lambda
好了,搞清楚了函数,现在再来看看 C++11 引入的 lambda 表达式,下面是一个简单的例子:
auto func = [](int x) // 定义一个lambda表达式
{
cout << x*x << endl; // lambda表达式的具体内容
};
func(3); // 调用lambda表达式
暂时不考虑代码里面的语法细节,单从第一印象上,我们可以看到有一个函数,但更重要的,是这个函数采用了赋值的方式,存入了一个变量。
**这就是 lambda 表达式与普通函数最大、也是最根本的区别。**因为 lambda 表达式是一个变量,所以,我们就可以“按需分配”,随时随地在调用点“就地”定义函数,限制它的作用域和生命周期,实现函数的局部化。
而且,因为 lambda 表达式和变量一样是“一等公民”,用起来也就更灵活自由,能对它做各种运算,生成新的函数。这就像是数学里的复合函数那样,把多个简单功能的小 lambda 表达式组合,变成一个复杂的大 lambda 表达式。
C++ 里的 lambda 表达式除了可以像普通函数那样被调用,还有一个普通函数所不具备的特殊本领,就是可以“捕获”外部变量,在内部的代码里直接操作。
int n = 10; // 一个外部变量
auto func = [=](int x) // lambda表达式,用“=”值捕获
{
cout << x*n << endl; // 直接操作外部变量
};
func(3); // 调用lambda表达式
看到这里,如果你用过 JavaScript,那么一定会有种眼熟的感觉。没错,lambda 表达式就是在其他语言中大名鼎鼎的**“闭包”**(closure),这让它真正超越了函数和函数对象。
使用Lambda函数的好处
零成本抽象。对!你没有看错。Lambda函数不会降低性能,它的性能和普通的函数一样好。
此外,Lambda函数使代码变得更加紧凑、更加结构化和更富有表现力。
使用 lambda 的注意事项
1.lambda 的形式首先你要知道,C++ 没有为 lambda 表达式引入新的关键字,并没有“lambda”这样的词汇,而是用了一个特殊的形式“[]”,术语叫“lambda 引出符”(lambda introducer)。
在 lambda 引出符后面,就可以像普通函数那样,用圆括号声明入口参数,用花括号定义函数体。下面的代码展示了我最喜欢的一个 lambda 表达式(也是最简单的):
auto f1 = [](){}; // 相当于空函数,什么也不做
这行语句定义了一个相当于空函数的 lambda 表达式,三个括号“排排坐”,看起来有种奇特的美感,让人不由得想起那句经典台词:“一家人最要紧的就是整整齐齐。”(不过还是差了个尖括号 <>)。
当然了,实际开发中不会有这么简单的 lambda 表达式,它的函数体里可能会有很多语句,所以一定要有良好的缩进格式——特别是有嵌套定义的时候,尽量让人能够一眼就看出 lambda 表达式的开始和结束,必要的时候可以用注释来强调。
auto f2 = []() // 定义一个lambda表达式
{
cout << "lambda f2" << endl;
auto f3 = [](int x) // 嵌套定义lambda表达式
{
return x*x;
};// lambda f3 // 使用注释显式说明表达式结束
cout << f3(10) << endl;
}; // lambda f2 // 使用注释显式说明表达式结束
你可能注意到了,在 lambda 表达式赋值的时候,我总是使用 auto 来推导类型。这是因为,在 C++ 里,每个 lambda 表达式都会有一个独特的类型,而这个类型只有编译器才知道,我们是无法直接写出来的,所以必须用 auto。
不过,因为 lambda 表达式毕竟不是普通的变量,所以 C++ 也鼓励程序员尽量“匿名”使用 lambda 表达式。也就是说,它不必显式赋值给一个有名字的变量,直接声明就能用,免去你费力起名的烦恼。
这样不仅可以让代码更简洁,而且因为“匿名”,lambda 表达式调用完后也就不存在了(也有被拷贝保存的可能),这就最小化了它的影响范围,让代码更加安全。
vector<int> v = {3, 1, 8, 5, 0}; // 标准容器
cout << *find_if(begin(v), end(v), // 标准库里的查找算法
[](int x) // 匿名lambda表达式,不需要auto赋值
{
return x >= 5; // 用做算法的谓词判断条件
} // lambda表达式结束
)
<< endl; // 语句执行完,lambda表达式就不存在了
2.lambda 的变量捕获
lambda 的“捕获”功能需要在“[]”里做文章,由于实际的规则太多太细,记忆、理解的成本高,所以我只说几个要点,帮你快速掌握它们:
-
“[=]”表示按值捕获所有外部变量,表达式内部是值的拷贝,并且不能修改;
-
“[&]”是按引用捕获所有外部变量,内部以引用的方式使用,可以修改;
-
你也可以在“[]”里明确写出外部变量名,指定按值或者按引用捕获,C++ 在这里给予了非常大的灵活性。
int x = 33; // 一个外部变量
auto f1 = [=]() // lambda表达式,用“=”按值捕获
{
//x += 10; // x只读,不允许修改
};
auto f2 = [&]() // lambda表达式,用“&”按引用捕获
{
x += 10; // x是引用,可以修改
};
auto f3 = [=, &x]() // lambda表达式,用“&”按引用捕获x,其他的按值捕获
{
x += 20; // x是引用,可以修改
};
class DemoLambda final
{
private:
int x = 0;
public:
auto print() // 返回一个lambda表达式供外部使用
{
return [this]() // 显式捕获this指针
{
cout << "member = " << x << endl;
};
}
};
3.泛型的 lambda
在 C++14 里,lambda 表达式又多了一项新本领,可以实现“泛型化”,相当于简化了的模板函数,具体语法还是利用了“多才多艺”的 auto:
auto f = [](const auto& x) // 参数使用auto声明,泛型化
{
return x + x;
};
cout << f(3) << endl; // 参数类型是int
cout << f(0.618) << endl; // 参数类型是double
string str = "matrix";
cout << f(str) << endl; // 参数类型是string
这个新特性在写泛型函数的时候非常方便,摆脱了冗长的模板参数和函数参数列表。如果你愿意的话,可以尝试在今后的代码里都使用 lambda 来代替普通函数,能够少写很多代码。