快试一试 lambda 表达式吧,让你的代码更简洁、更漂亮!

目录

lambda 表达式

定义 lambda 表达式

捕获子句

按值捕获

按引用捕获

捕获特定的变量

捕获this指针

使用lambda的优势


lambda 表达式

lambda 表达式提供了一种便捷、简洁的语法来快速定义回调函数或函数对象。而且,不只语法简洁,lambda 表达式还允许在使用回调的地方定义回调的逻辑。这通常比在某个类定义的函数调用运算符的某个地方定义这种逻辑要好得多。因此,使用 lambda 表达式一般能够得到极具表达力且可读性仍然很好的代码。

lambda 表达式与函数定义有许多相似的地方。最基本的 lambda 表达式提供了一种定义没有名称的函数(即匿名函数)的方法。在应用中会发现 lambda 表达式与普通函数不同,因为 lambda 表达式可以访问自己定义的作用域内存储的变量。lambda 表达式的计算结果是一个函数对象。该函数对象的正式名称是 lambda 闭包,但是有很多人也称之为 lambda 函数。

 

 

定义 lambda 表达式

一个基本的 lambda 表达式:

[] (int x, int y) { return x < y; }

可以看出,lambda 表达式的定义看上去很像函数的定义。主要区别在于,lambda 表达式不指定返回类型和函数名称,并且始终以方括号开头。左边的方括号称为 lambda 引导,它们标记了 lambda 表达式的开头。lambda 引导的内容并不总是为空的。lambda 引导后根的圆括号是 lambda 参数列表。对于没有参数的 lambda 表达式,可以省略空的参数列表 ()。即可以将形式为 [](){......},的 lambda 表达式进一步缩减为 []{......}。空的lambda引导不能省略,因为它标记lambda表达式的开始。

有的人可能会觉得 lambda 表达式没有函数名称还可以理解,但是没有返回类型是不是就有点过分了(玩呢?)。lambda 表达式可以包含任意数量的语句,返回类型默认为返回值的类型。如果没有返回值,返回类型为 void。不过返回值类型也是可以根据需求设定的,例如:

[](double x) -> int { int y = x; return x-y;}

 

 

捕获子句

lambda 引导 [] 不一定是空的,它可以包含捕获子句,以指定封闭作用域中的变量如何在 lambda 表达式中访问。如果方括号为空,则lambda 表达式体只能使用 lambda 表达式中局部定义的实参和变量。没有捕获子句的 lambda 表达式称为无状态的 lambda 表达式,因为它不能访问其封闭作用域中的任何内容。默认捕获子句有两种:= 和 &。捕获子句只能包含一种默认捕获子句,不能同时包含两者。

 

按值捕获

如果在方括号中包含 =,lambda 表达式体就可以按值访问封闭作用域中的所有自动变量,即这些变量的值可以在 lambda 表达式中使用,但不能修改存储在原始变量中的值。当你按值捕获变量时,又试图改变它,编译器就会报错:

int a = 1,b = 2,c = 3;
[ =,&c ](){ a++; b++; c++; cout << a << " " << b << " " << c <<endl; } ();
cout << a << " " << b << " " << c <<endl;

= 捕获子句允许在 lambda 表达式体中按值访问 lambda 表达式定义所在作用域的所有变量。在上面的例子中,在原则上,lambda 表达式体能够访问 main() 的3个局部变量:a、b 和 c。按值捕获局部变量的效果与按值传递实参大不相同。

不同点在于:对于 lambda 表达式体内用到的封闭作用域内的每个局部变量,闭包对象都有一个成员。称为 lambda 捕获了这些变量。至少在概念上,所生成的成员变量的名称与捕获到的变量的名称相同。这样一来,lambda 表达式体看起来访问的是封闭作用域中的变量,但实际上访问的是 lambda 闭包内存储的对应的成员变量。

 

按引用捕获

如果在方括号中放置 &,封闭作用域中的所有变量就可以按引用访问,所以它们的值可以由lambda表达式体中的代码修改。例如:

int a = 1,b = 2,c = 3;
//按引用捕获
[ & ](){ a++; b++; c++; } ();
cout << a << " " << b << " " << c <<endl;

外层作用域内的所有变量都可按引用访问,所以 lambda 可以使用和修改它们的值。虽然 & 捕获子句是合法的,但是在外层作用域按引用捕获所有变量并不是一种好方法,因为这将使得变量有可能被无意修改。类似地,使用 = 默认捕获子句则可能引入高开销的复制操作。因此,更安全的做法是显式指定如何捕获自己需要的每个变量。

 

捕获特定的变量

通过在捕获子句中逐个列举,可以指定想要访问的封闭作用域中的特定变量。对于每个变量,可以选择按值还是按引用捕获。在变量名前面加上 &,就可以按引用捕获变量。例如:

auto counter { [ &count] (int x, int y) {++count; return x < y;} };

count 是封闭作用域中可以在 lambda 表达式体中访问的唯一变量,&count 规范使之可以按引用访问。没有 &,外层作用域中的 count 变量就按值访问。当你想要按值捕获特定变量时,不能在变量名前面加 = 作为前缀。例如,捕获子句[ = numbers ]是无效的,正确的语法是[ numbers ]。

在捕获子句中放置多个变量时,就用逗号分开它们。可以自由混合按值捕获的变量和按引用捕获的变量。还可以在捕获子句中,同时包含默认捕获子句和捕获的特定变量名称。例如:捕获子句[ =, &counter ]允许按引用访问 counter,按值访问封闭作用域中的其他变量。类似的,捕获子句[ &, numbers ]的意思是,按值捕获 numbers,按引用捕获其他变量。如果指定默认捕获子句( = 或 &),则它必须是捕获列表中的第一项。

注意:如果使用 = 默认捕获子句,则不能再按值捕获任何特定变量;类似地,如果使用 &,则不能再按引用捕获特定变量。例如:[ =, = a ] 或 [ &, & b ]。

 

捕获this指针

当我们在类中的成员函数中使用 lambda 表达式时,这个情况和之前的不一样,问题在于只有局部变量和函数实参才能按值或引用捕获,而类的成员变量不能按值或按引用捕获。以下是在类的成员函数中使用 lambda 的情况:

可以通过对捕获子句添加关键字 this ,让 lambda 表达式访问类的成员。通过捕获 this 指针,实际上就使得 lambda 表达式能够访问包含它的成员函数所能访问的所有成员,也就是说,尽管 lambda 闭包不属于类,但其函数调用运算符仍然能够访问类的 protected 和 private 数据成员。lambda 表达式还能够访问所有成员函数,无论它们被声明为 public、protected 还是 private。

class Asd
{
	private:
		int a, b;
	public:
		Asd(){
			a = 1;
			b = 2; 
		}
		int geta(){
			return a;
		}
		int getb(){
			return b;
		}
		void print(){
			auto asd = [this](){ return getb() - geta();} ;
			cout << "getb() - geta():" << asd() <<endl;
		}
		void print1(){
			auto asd = [=](){ return b - a;} ;
			cout << "b-a:" << asd() <<endl;
		}
};

注意:= 默认捕获子句已经暗示着(按值)捕获this指针。因此这条语句也是合法的:

auto asd = [=](){ return b - a;} ;

但不允许将默认捕获子句 = 与 this结合使用 (至少在C++17中不允许,C++20中可能会有变化) 。因此,编译器将把[=, this]这种形式的捕获子句标记为错误。不过,允许使用[&, this],因为 & 并不暗示着捕获this。

完整代码:

#include <iostream>
using namespace std;

class Asd
{
	private:
		int a, b;
	public:
		Asd(){
			a = 1;
			b = 2; 
		}
		int geta(){
			return a;
		}
		int getb(){
			return b;
		}
		void print(){
			auto asd = [this](){ return getb() - geta();} ;
			cout << "getb() - geta():" << asd() <<endl;
		}
		void print1(){
			auto asd = [=](){ return b - a;} ;
			cout << "b-a:" << asd() <<endl;
		}
};

int main()
{
	//不捕获任何变量
	[] { cout << "Study lambda!" <<endl; } ();
	auto print = [] { cout << "Study lambda!" <<endl; };	
	print();

	//根据需求确定返回值
	auto asd = [](double x) -> int { int y = x; return x-y;};
	cout << asd(3.6) <<endl;

	int a = 1,b = 2,c = 3;
	//按值捕获
	[ = ](){ cout << a << " " << b << " " << c <<endl; } ();

	//按引用捕获
	[ & ](){ a++; b++; c++; } ();
	cout << a << " " << b << " " << c <<endl;

	//捕获特定的变量
	[ =,&c ](){ c++; cout << a << " " << b << " " << c <<endl; } ();
	cout << a << " " << b << " " << c <<endl;

	[ &,a ](){ b++; c++; cout << a << " " << b << " " << c <<endl; } ();
	cout << a << " " << b << " " << c <<endl;

	//捕获this指针
	Asd asd1, asd2;
	asd1.print();
	asd2.print1();

	return 0;
}

 

 

使用lambda的优势

接下来将从距离简洁两个方面探讨使用 lambda 的优势。

很多人认为,让定义位于使用的地方附近很有用。这样,在阅读源代码时,就无需去找该函数功能的具体代码。例如,调用 count_if()的第三个参数时,不需要向前寻找具体实现。另外,如果需要修改代码,涉及的内容都在附近。从这一角度出发,lambda 是理想的选择,因为其定义和使用是在同一个地方进行的。而函数可能存在一种比较糟糕的情况,即其函数内部使用了其他函数,而这些函数可能在不同的地方,这在阅读源码这一点上是非常费时、费力的。

从简洁的角度看,函数和lambda的简洁程度相当,一个例外是,需要使用同一个 lambda 两次:

count1 = count_if ( a.begin(), a.end(), [] (int x) { return x % 3 == 0; });
count2 = count_if ( b.begin(), b.end(), [] (int x) { return x % 3 == 0; });

但并不一定要编写lambda两次,而是给lambda指定一个名称,并使用该名称两次:

auto mod3 = [] (int x) { return x % 3 == 0; }
count1 = count_if ( a.begin(), a.end(), mod3 );
count2 = count_if ( b.begin(), b.end(), mod3 );

甚至可以像使用常规函数那样使用有名称的 lambda:

bool result = mod3(z);

然而,不同于常规函数,可在函数内部定义有名称的 lambda 。mod3 的实际类型随实现而异,它取决于编译器使用什么类型来跟踪lambda 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章