Lambda表达式概述
C++11引入了Lambda表达式,用于定义并创建匿名的函数对象,主要用于方便编程,避免全局变量的定义,并且变量安全。
Lambda 表达式可以理解为一个匿名的内联函数。一个Lambda表达式表示一个可调用的代码单元。
Lambda 的基本语法:
[函数对象参数](函数参数)修饰符->返回值类型{函数体};
可以忽略函数参数和返回类型,但必须永远包含函数对象参数和函数体;忽略参数列表等价于指定一个空函数列表,忽略返回类型,Lambda 会根据函数体中的代码推断出来(如果函数体直接return,则是void类型)。
和普通函数一样,Lambda 表达式的调用方式与普通函数的调用方式相同,也都具有一个返回类型、一个参数列表和一个函数体。
与普通函数的几点不同在于:
(1)Lambda必须使用尾置返回类型。
(2)lambda表达式不能有默认参数。因此,一个Lambda表达式调用的实参数目永远与形参数目相等。
(3)所有参数必须有参数名。
(4)不支持可变参数。
Lambda 的使用
1、函数对象参数(捕获列表)
[函数对象参数] (函数参数) 修饰符 -> 返回值类型 {函数体};
[ ]
标识一个Lambda表达式的开始,这一部分是不可以忽略的。
函数对象参数表示捕获列表,捕获就是明确的指明 Lambda 能使用的局部变量。(在捕获全局变量时会提示:无法在 lambda 中捕获带有静态存储持续时间的变量)
捕获列表只用于非静态局部变量,Lambda可以直接使用静态局部变量和在函数之外声明的名字(全局变量)。
void func()
{
static int i = 10;
int j = 20;
auto f1 = [ ] () { return j; }; //编译出错,没有进行捕获,函数体内不能使用
auto f2 = [ ] () { return i; }; //静态变量无需捕获
auto f3 = [j] () { return j; }; //进行了捕获,函数体内可以使用了
};
函数参数有以下几种形式:
捕获列表 | 意义 |
---|---|
[ ] | 不捕获Lambda表达式外的变量。 |
[=] | 以值传递的方式捕获 Lambda 所在范围内所有可见的局部变量,即以const引用的方式传值 。 |
[&] | 以引用传递的方式使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this)。 |
[this] | 函数体内可以使用 Lambda 所在类中的成员变量。 |
[x] 或 [=x] | 以值传递的方式捕获变量x,即const int x。 |
[&x] | 将变量x按引用进行传递,在函数体内可以改变 x 的值。 |
[x,&x1] | 将变量x按值传递,变量x1按引用进行传递。 |
[=,&x,&x1] | 除变量x和变量x1按引用进行传递外,其他参数都按值进行传递。 |
[&,x,x1] | 除变量x和变量x1按值进行传递外,其他参数都按引用进行传递。 |
(1)值捕获
与传递参数类似,采用值捕获的前提是变量可以拷贝。
与参数不同,被捕获的变量的值是在 Lambda 创建时拷贝,而不是调用时拷贝。例如:
void func()
{
int v1 = 42;
auto f = [ v1 ] { return v1; }; // 创建时拷贝,f() = 42
v1 = 24;
auto j = f(); // 调用f()
cout << j << endl; // 42,f保存了我们创建它时v1的拷贝
}
由于被捕获的值在 Lambda 创建时拷贝,因此在随后对其修改不会影响到 Lambda 内部对应的值。
默认情况下,如果以传值方式捕获外部变量,则在 Lambda 表达式函数体中不能修改该外部变量的值。
要改变值捕获的局部变量,就必须在参数列表后加上关键字mutable
。
语法变为:[capture list] (parameter list) mutable -> return type {function body}
void func()
{
int v1 = 42;
int v2 = 24;
auto f1 = [ = ]() mutable { v1 = v2; return v1 + v2 ; }; // 创建时拷贝 f1() = 48
cout << v1 << endl; // 42
cout << f1() << endl; // 48
cout << v1 << endl; // 42
v2 = 42;
cout << f1() << endl; // 48
}
可以看出,被 mutable
修饰的函数参数,该函数参数可以在 Lambda 函数体内改变,但不会改变函数体外该变量的值,我们也可以理解为在函数体内拷贝了这个变量的同名变量。
(2)引用捕获
和函数引用参数一样,一个引用类型的变量在函数体内改变时,实际上使用的是引用所绑定的对象。
void func()
{
int v1 = 42;
auto f = [ &v1 ] { return v1; }; // 引用捕获,将v1拷贝到名为f的可调用对象
cout << f() << endl; // 42
v1 = 24;
auto j = f();
cout << j << endl; // 24
}
void func()
{
int v1 = 42;
auto f = [ &v1 ] { v1 = 24; return v1; }; // 引用捕获,将v1拷贝到名为f的可调用对象
auto j = f();
cout << j << endl; // 24
cout << v1 << endl; // 24
}
如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 Lambda 执行的时候是存在的。
Lambda 捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 Lambda 可能在函数结束后执行,这里就会出现问题。
有一些不可拷贝对象,只能使用引用捕获的方式,比如 ostream 对象。
(3)隐式捕获
除了显式指定我们希望使用的来自所在函数的局部变量之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
隐式捕获有两种方式,分别是[=]
和[&]
,分别表示以值传递和引用传递的方式捕获所在函数的局部变量。
void func()
{ // 引用传递
int v1 = 42;
int v2 = 24;
auto f1 = [ & ] { return v1 + v2 ; }; // 引用传递所有可见局部变量
cout << f1() << endl; // 66
v1 = 24;
cout << f1() << endl; // 48
}
(4)混合方式捕捉
混合捕获时,捕获列表中的 第一个元素必须是 =
或 &
,此符号 指定了默认捕获的方式 是值捕获或引用捕获 。
此外,显式捕获的变量必须使用和默认捕获不同的方式捕获。
void func()
{
int i = 10;
int j = 20;
auto f1 = [ =, &i ] () { return j + i; }; //正确,默认值捕获,显示是引用捕获
auto f2 = [ =, i ] () { return i + j; }; //编译出错,默认值捕获,显示值捕获,冲突了
auto f3 = [ &, &i ] () { return i +j; }; //编译出错,默认引用捕获,显示引用捕获,冲突了
};
2、函数参数
[函数对象参数] (函数参数) 修饰符 -> 返回值类型 {函数体};
这一部分可以被省略(如果函数无参数),我们可以使用下面方式定义Lambda表达式:
void func()
{
auto f1 = [] {
cout << "Hello" << '\n';
};
f1();
auto f2 = [] (string s) {
cout << "Hello " << s << '\n';
};
f2("world");
}
3、修饰符
[函数对象参数] (函数参数) 修饰符 -> 返回值类型 {函数体};
这一部分是可以省略的,常见的修饰符有两个,一个是mutable
,另一个是exception
。
mutable
用来在 Lambda 函数体内改变通过值传递捕获的变量,但不会改变在函数体外该变量的值。exception
声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。
4、返回值类型
[函数对象参数] (函数参数) 修饰符 -> 返回值类型 {函数体};
这一部分也是可以省略的,Lambda 表达式会自动推断返回值类型,但是返回类型不统一会报错。
5、函数体
[函数对象参数] (函数参数) 修饰符 -> 返回值类型 {函数体};
标识函数的实现,这一部分可以为空,但是不能省略。
Lambda 的调用
1. 创建时调用
在函数体后面加括号(如果有函数参数则需要参数),则代表在创建时调用表达式:
void func()
{
int v1 = 42;
auto f = [ &v1 ] { return v1; }(); // 将括号放到函数体后面,相当于调用,然后给 f 赋值
cout << f << endl; // 42
v1 = 24;
auto j = f;
cout << j << endl; // 42,与上述引用捕获的例子形成对比
}
2. 创建后,像普通函数一样调用(匿名函数实例化)
void func()
{
int v1 = 42;
auto f = [ &v1 ] { return v1; }; // 创建
cout << f() << endl; // 调用,42
v1 = 24;
auto j = f();
cout << j << endl; // 调用,24
}
auto f = [ &v1 ] { return v1; };
等价于
#include <functional>
function<int()> f = [ &v1 ] { return v1; };
3. 使用函数指针
使用函数指针指向 Lambda 表达式,但是不能有变量捕获。
void func()
{
auto f = [](int x) { cout << x << endl; return x*x; }; // 创建
int (*func_ptr)(int) = f; // 函数指针
cout << func_ptr(3) << endl; // 9
}
举例
class Increase
{
public:
void operator()(int& val){++val;}
};
int main()
{
int i = 1, j = 1;
auto f = [](int& j){ ++j; }; // 等价于Increase
f(j); // (1)
Increase Inc;
Inc(i); // (2)
} // (1)与(2)等价
class Bigger//1
{
public:
bool operator()(int a, int b){ return a > b; }
};
bool bigger(int a, int b){ return a > b; }
int main()
{
vector<int> vec{ 2, 0, 1, 3, 3, 0, 1, 9, 7, 7 };
sort(vec.begin(), vec.end(), [](int a, int b)->bool{return a > b; }); // lambda
sort(vec.begin(), vec.end(), bigger); // 函数指针
sort(vec.begin(), vec.end(), Bigger()); // 函数对象
}
class F
{
public:
F(int n) :num(n){}
int operator()(){ return num; }
private:
int num;
};
int main(){
int num = 100;
auto f = [num](){return num; };
cout << f() << endl; // 100
F fclass(num);
auto j = fclass();
cout << j << endl; // 100
return 0;
}