C++11:lambda表达式

lambda基础使用

  lambda 表达式(lambda expression)是一个匿名函数,lambda 表达式基于数学中的 λ 演算得名。C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。

lambda表达式的基本构成:
image.png

[capture list] (params list) mutable exception-> return type { function body }

① 函数对象参数

  [] 标识一个 lambda 的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 lambda 为止时 lambda 所在作用范围内可见的局部变量(包括lambda所在类的this)。函数对象参数有以下形式:

  • :没有使用任何函数对象参数
  • =:函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)
  • &:函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)
  • this:函数体内可以使用lambda所在类中的成员变量
[&a]:将a按引用进行传递
[a, &b]:将a按值进行传递,b按引用进行传递
[=,&a, &b]:除a和b按引用进行传递外,其他参数都按值进行传递
[&, a, b。除a和b按值进行传递外,其他参数都按引用进行传递]

注意:捕捉列表是不允许变量重复传递,例如
[=, a]:这里 = 已经以值传递的方式捕捉了所有变量,捕捉 a 重复
[&, &this]:这里 & 已经以引用传递的方式捕捉了所有变量,再次捕捉 this 也是一种重复

② 参数列表

  标识重载的()操作符的参数,与普通函数的参数列表相同,没有参数时,可以连同括号一起省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。

③ 可修改标示符

  mutable 声明,这部分可以省略。默认情况下,lambda 函数总是一个 const 函数,按值传递函数对象参数时,加上 mutable 修饰符后,可以取消其常量性,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。

④ 错误抛出标示符

  exception 声明,这部分也可以省略。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)

⑤ 函数返回值

   ->return-type:返回值类型。标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

⑥ 函数体

  内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量

class Test {
public:
	void func(int x, int y) {
		auto x0 = [] {};
		//auto x1 = [] {return i; }; //err, 没有捕获外部变量
		auto x2 = [=] {return i + x + y; }; //ok, 值传递方式捕获所有外部变量
		auto x3 = [&] {return i + x + y; };//ok, 引用传递方式捕获所有外部变量
		auto x4 = [this] {return i; }; //ok, 捕获this指针
		//auto x5 = [this] {return i + x + y};//err, 没有捕获x, y
		auto x6 = [this, x, y] {return i + x + y; };//ok, 捕获this指针, x, y
		auto x7 = [this] {return i++; };//ok, 捕获this指针, 并修改成员的值
	}
public:
	int i = 0;
};

void test1() {
	int a = 0, b = 1;
	//auto f1 = [] {return a; };//err, 没有捕获外部变量
	auto f2 = [a] {return a; }; //ok, 值传递方式捕获 a 变量
	auto f3 = [=] {return a + b; }; //ok, 值传递方式捕获所有外部变量
	//auto f4 = [=] {return a++; };//err, a是以赋值方式捕获的,无法修改
	auto f5 = [a]() mutable {return a++; };/ok, 加上mutable修饰符后,可以修改按值传递进来的拷贝
	auto f6 = [&] {return a++; };//ok, 引用传递方式捕获所有外部变量, 并对a执行自加运算
	//auto f7 = [a] {return a + b; };//err, 没有捕获变量b
	auto f8 = [a, &b] {return a + (b++); };  //ok, 捕获a, &b
	auto f9 = [=, &b] {return a + (b++); };//ok, 捕获所有外部变量,&b
}

值传递与引用传递的区别

int main() {
	int j = 12;
	auto by_val_lambda = [=] { return j; };
	auto by_ref_lambda = [&] { return j; };
	cout << "by_val_lambda: " << by_val_lambda() << endl;
	cout << "by_ref_lambda: " << by_ref_lambda() << endl;
	cout << "j = " << j << endl;
	j++;

	cout << "by_val_lambda: " << by_val_lambda() << endl;
	cout << "by_ref_lambda: " << by_ref_lambda() << endl;
	cout << "j = " << j << endl;
	return 0;
}

运行结果如下所示:
image.png

  在这个例子中当 by_val_lambda 捕获到 a 的值时就已经被赋值在其中不会被更改了,无论后面如何修改 a 的值, by_val_lambda的返回结果都不会改变。但是 by_ref_lambda 的返回结果会随着捕获变量的值改变而改变。

lambda与仿函数

  在 STL中会用到一种特殊的对象,通常称为函数对象,或者仿函数。就是重载了 () 运算符的自定义类型对象。仿函数的具体使用这里就不做过多赘述。

class MyFunctor {
public:
	int operator()(int x, int y) { return x + y; }
};
int main() {
	int a = 1, b = 2;
	MyFunctor add;
	cout << add(a, b) << endl;
	return 0;
}

上述代码在使用的时候其实与仿函数非常相似,当他们放在一起时如下所示:

class MyFunctor {
public:
	MyFunctor(int x) :round(x) {}
	int operator()(int tmp) { return tmp + round; }
private:
	int round;
};

void test2() {
	//仿函数
	int round = 2;
	MyFunctor add1(round);//调用构造函数
	cout << "result1 = " << add1(1) << endl; //operator()(int tmp)

	 //lambda表达式
	auto add2 = [round](int tmp)->int {return tmp + round; };
	cout << "result2 = " << add2(1) << endl;
}

  在该例中,分别使用了仿函数与 lambda 两种方式实现的加法。lambda 函数捕捉了 round 变量,而仿函数则通过 round 初始化类。其它的,如在传参上二者保持一致,出去再语法层面上的不同,而这有着相同的内涵--都可以捕捉一些变量作为初始状态,并接受参数进行运算。

  而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。

Lambda与static inline函数

  lambda 函数可以省略外部声明的 static inline 函数,其相当于一个局部函数。局部函数仅属于父作用域,比起外部的 static inline 函数,或者是自定义的宏,lambda 函数并没有实际运行时的性能优势(但也不会差),但是 lambda 函数可读性更好。
  父函数结束后,该 lambda 函数就不再可用了,不会污染任何名字空间。

lambda类型

  lambda 表达式的类型在 C++ 11 中被称为“闭包类型”,每一个 lambda 表达式则会产生一个临时对象(右值)。因此,严格地将,lambda 函数并非函数指针。

  不过 C++ 11 标准却允许 lambda 表达式向函数指针的转换,但提前是 lambda 函数没有捕获任何变量,且函数指针所示的函数原型,必须跟 lambda 函数函数有着相同的调用方式。

void test3() {
	int a = 3, b = 4;
	auto totalChild = [] (int x, int y)->int{return x + y; };
	typedef int (*allChild)(int, int);
	typedef int (*oneChild)(int);

	allChild p = totalChild;
	//oneChild q = totalChild; 错误,参数类型必须一致

	decltype(totalChild) p2 = totalChild; //需要通过 decltype 获得lambda 的类型
	//decltype(totalChild) p3 = p; //错误,指针无法转化为 lambda
}

  所以,没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。

lambda 与 STL

  将 lambda 应用到 STL 的相关算法中,可以极大程度的简化别写的难度,提升代码的阅读质量。以使用 for_each 为例。

vector<int> nums;
vector<int> largeNums;
class LNums {
public:
	LNums(int u) : ubound(u) {} //构造函数

	void operator () (int i) const {//仿函数
		if (i > ubound) {
			largeNums.push_back(i);
		}
	}
private:
	int ubound;
};

void test4() {
	//初始化数据
	for (int i = 0; i < 10; i++)
		nums.push_back(i);
	
	int ubound = 5;
	//1、传统的for循环
	for (auto i = nums.begin(); i != nums.end(); i++)
		if (*i > ubound)
			largeNums.push_back(*i);

	//2、使用仿函数
	for_each(nums.begin(), nums.end(), LNums(ubound));

	//3、使用lambda函数和算法for_each
	for_each(nums.begin(), nums.end(), [=](int i) {
		if (i > ubound)
			largeNums.push_back(i);
		});

	//4、遍历元素
	for_each(largeNums.begin(), largeNums.end(), [=](int i) {
		cout << i << " ";
		});
	cout << endl;
}