前言
lambda式作为一种创建函数对象的手段,实在太过方便,对c++日常软件开发产生极大影响,所以特来学习。
一、lambda函数基本使用
lambda函数是C++11标准新增的语法糖,也称为lambda表达式或匿名函数。
lambda函数的特点是:距离近、简洁、高效和功能强大。
示例:
[](const int& no) -> void {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; };
语法:
参数列表
参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,()可以省略不写。
与普通函数的不同:
- lambda函数不能有默认参数。
- 所有参数必须有参数名。
- 不支持可变参数。
返回类型
用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,编译器会根据函数体中的代码推断出来。
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。
auto f=[](const int& no) ->double{
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
};
此时auto判定f为double类型;
函数体
类似于普通函数的函数体。
捕获列表
通过捕获列表,lambda函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,不能访问全局变量)。
捕获列表书写在[]中,与函数参数的传递类似,捕获方式可以是值和引用。
以下列出了不同的捕获列表的方式。
值捕获
与传递参数类似,采用值捕获的前提是变量可以拷贝。
与传递参数不同,变量的值是在lambda函数创建时拷贝,而不是调用时拷贝。
例如:
size_t v1 = 42;
auto f = [ v1 ] {
return v1; }; // 使用了值捕获,将v1拷贝到名为f的可调用对象。
v1 = 0;
auto j = f(); // j为42,f保存了我们创建它是v1的拷贝。
由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值。
默认情况下,如果以传值方式捕获变量,则在lambda函数中不能修改变量的值。
引用捕获
和函数引用参数一样,引用变量的值在lambda函数体中改变时,将影响被引用的对象。
size_t v1 = 42;
auto f = [ &v1 ] {
return v1; }; // 引用捕获,将v1拷贝到名为f的可调用对象。
v1 = 0;
auto j = f(); // j为0。
如果采用引用方式捕获变量,就必须保证被引用的对象在lambda执行的时候是存在的。
隐式捕获
除了显式列出我们希望使用的父作域的变量之外,还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。
int a = 123;
auto f = [ = ] {
cout << a << endl; }; //值捕获
f(); // 输出:123
auto f1 = [ & ] {
cout << a++ << endl; }; //引用捕获
f1(); //输出:123(采用了后++)
cout << a << endl; //输出 124
混合方式捕获
lambda函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。
混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获。
需要注意的是:显式捕获的变量必须使用和默认捕获不同的方式捕获。例如:
int i = 10;
int j = 20;
auto f1 = [ =, &i] () {
return j + i; }; // 正确,默认值捕获,显式是引用捕获
auto f2 = [ =, i] () {
return i + j; }; // 编译出错,默认值捕获,显式值捕获,冲突了
auto f3 = [ &, &i] () {
return i +j; }; // 编译出错,默认引用捕获,显式引用捕获,冲突了
修改值捕获变量的值
在lambda函数中,如果以传值方式捕获变量,则函数体中不能修改该变量,否则会引发编译错误。
在lambda函数中,如果希望修改值捕获变量的值,可以加mutable选项,但是,在lambda函数的外部,变量的值不会被修改。
int a = 123;
auto f = [a]()mutable {
cout << ++a << endl; }; // 不会报错
cout << a << endl; // 输出:123
f(); // 输出:124
cout << a << endl; // 输出:123
异常说明
lambda可以抛出异常,用throw(…)指示异常的类型,用noexcept指示不抛出任何异常。
二、lambda表达式使用的注意事项
避免默认捕获模式
按引用的默认捕获模式可能导致空悬引用,一旦由lambda式所创建的闭包越过了局部变量或形参的生命周期,那么闭包内的引用就会空悬(即必须保证被引用的对象在lambda执行的时候是存在的)
(有没有空悬引用其实就是看的生命周期,那个长)
既然引用有导致空悬引用的风险,那是不是可以用按值捕获呢。按值的默认捕获也有可能存在空悬的风险。如按值捕获了一个指针以后,在lambda式创建的闭包中持有的是这个指针的副本,但并无办法阻止lambda式之外的代码去针对该指针实施delete操作所导致的指针副本空悬。
对于类的方法中使用lambda,如果使用到了类的成员变量,则会出现无法被捕获的错误。如下:
void Widget::addFilter() const
{
filters.emplace_back(
[divisor](int value) //错误
{
return value % divisor==0;} //局部没有可捕获的divisor(divisor既不是局部变量,也不是形参)
);
}
解决这一问题,关键在于一个裸指针隐式应用,这就是this。每一个非静态成员函数都持有一个this指针,然后每当提及该类的成员变量时都会用到这个指针。
所以此上的代码的lambda函数被捕获的实际上是Widget的this指针,而不是divisor。
代码如下 :
void Widget::addFilter() const
{
auto currentObjectPtr=this;
filters.emplace_back(
[currentObjectPtr](int value)
{
return value%currentObjectPtr->divisor==0;}
);
}
这就相当于lambda闭包的存活与它含有其this指针副本的Widget对象的生命期是绑在一起的
对于以static声明的静态变量,可以在lambda内使用,但是它们不能被捕获
三、lambda表达式底层实现原理
class Add{
public:
Add(int n):_a(n){
}
int operator()(int n){
return _a + n;
}
private:
int _a;
};
int main(){
int n = 2;
Add a(n);
a(4);
auto a2 = [=](int m)->int{
return n + m; };
a2(4);
return 0;
}
从上面的代码中可以看到,仿函数与lambda表达式完全一样
实际当我们编写了一个lambda表达式之后,编译器将该表达式翻译成一个未命名类的未命名对象。该类含有一个operator()。
整个lamda表达式,编译的时候,
- 编译器给你自动生成一个形如 <lambda_b328511335c7e943aa98460a349659c7> 的类
- 然后把捕获列表中的参数,都按照你的要求(值捕获, 引用捕获)包装到这个类的成员里面
- 编译器生成一个 operator() 重载函数, 最后你对lamda的调用就是对函数对象的调用了, 捕获的参数早给你准备好了
采用值捕获
采用值捕获时,lambda函数生成的类用捕获变量的值初始化自己的成员变量。
例如:
int a =10;
int b = 20;
auto addfun = [=] (const int c ) -> int {
return a+c; };
int c = addfun(b);
cout << c << endl;
等同于:
class Myclass
{
int m_a; // 该成员变量对应通过值捕获的变量。
public:
Myclass( int a ) : m_a(a){
}; // 该形参对应捕获的变量。
// 重载了()运算符的函数,返回类型、形参和函数体都与lambda函数一致。
int operator()(const int c) const
{
return a + c;
}
};
默认情况下,由lambda函数生成的类是const成员函数,所以变量的值不能修改。如果加上mutable,相当于去掉const。这样上面的限制就能讲通了。
采用引用捕获
如果lambda函数采用引用捕获的方式,编译器直接引用就行了。
唯一需要注意的是,lambda函数执行时,程序必须保证引用的对象有效。
转载:https://blog.csdn.net/weixin_52259848/article/details/128401710