一、lambda

1. lambda表达式语法
  1. lambda表达式本质是一个匿名函数对象(这个原理部分会讲到),不过与普通函数只能定义在全局或类内部不同,它可以直接定义在函数内部。
  2. lambda表达式格式:

代码语言:javascript

AI代码解释

[capture_list] (parameters) -> return type { function boby };
  1. [capture_list]:捕捉列表,存在于lambda函数起始位置,它的存在能让编译器识别出这是一个lambda函数。作用:捕捉列表能够捕捉上下文里的局部变量和参数供lambda函数使用(全局变量、静态变量可以直接使用)。即使什么都不捕捉捕捉列表也不能省略。因为编译器要通过它来识别这个lambda函数。
  2. (parameters):参数列表,和普通函数一样参数列表功能一样,不过如果不用传参且不显式指定返回值类型的话()是能够省略的,但如果使用了之后会讲到的mutable修饰符参数列表便不可省略
  3. -> return type:返回值类型,这里是返回值类型的尾置语法,也就是把返回值类型放在了参数列表的后面。如果没有返回值的话这里可以省略。也可以不写纯靠编译器推理出这里的返回值类型
  4. { function boby }:函数体:这里和普通函数没有什么区别。可以在里面使用捕捉列表捕捉到的变量、传递来的形参、在内部定义的变量,以及全局变量、静态变量。函数体为空也不能省略。
  5. lambda表达式在编译时会被转换为一个匿名类,这个类的实例就是lambda函数对象。我们通过这个匿名函数对象来调用lambda。
  6. lambda表达式对于语法使用层面没有类型,我们一般是通过auto或者是模板参数定义的对象(都可以依靠编译器去推理)去接收lambda对象。

代码语言:javascript

AI代码解释

auto add = [](int x, int y)->int { return x + y; };
int n = add(1, 2);
cout << n << endl;

在这里插入图片描述

在这里插入图片描述

  1. [](int x, int y)->int { return x + y; };是一个lambda表达式,也就是一个匿名函数对象,我通过变量add来接收这个对象。之后通过add来调用这个lambda函数。
2. 捕捉列表和mutable关键字
  1. lambda表达式默认是只能使用lambda函数体和参数列表中的变量以及全局变量,静态变量。如果想要用外层作用域中的变量就要进行捕捉。
  2. 第一种捕捉方式是显式在捕捉列表里传值捕捉和传引用捕捉。捕捉的多个变量之间需要用,(逗号)进行分割,比如[ x, y, &z],其中x和y是传值捕捉,z是传引用捕捉。
  3. 第二种捕捉方式是隐式捕捉,它也包含传值捕捉和传引用捕捉,捕捉列表里是=就是传值捕捉,是&就是传引用捕捉。它们会根据函数体内使用了外层作用域的哪些变量来进行自动捕捉,也就是编译器自动捕捉所有使用到的变量。[=]与[&]
  4. 第三种捕捉方式是混合捕捉,也就是显式捕捉和隐式捕捉结合在一起使用。不过隐式捕捉=或者&一定要是第一个元素,且能和=混合使用的一定是显式引用捕捉,和&混合使用的一定是显式传值捕捉。比如[=, &x, &y],[&, x, y]。
  5. lambda表达式如果在函数局部域中,他可以捕捉lambda表达式之前所定义的变量,但不能捕捉全局变量和静态变量,这两个不用捕捉就能直接使用。这同时也意味着,如果lambda表达式如果定义在全局,捕捉列表必须为空。

在这里插入图片描述

在这里插入图片描述

  1. 传值捕捉不能通过修改这个捕捉来的变量来影响外层作用域的变量(因为是副本),但传引用捕捉是直接修改外层作用域变量的(除非原变量不可修改)
  2. 由于lambda捕捉列表是被const修饰的,也就是说传值捕捉过来的变量是不能进行修改的,如果要修改则需要在捕捉列表后面加上修饰符mutable,这样可以取消它的常量性,但由与传值捕捉的是副本,所以就算可以修改这个捕捉过来的变量也是不能够影响到外层作用域的变量的(实参)
  3. 使用mutable修饰符后,参数列表即使为空也不能够省略。

代码语言:javascript

AI代码解释

int a = 1, b = 2;
auto add = [a, &b]()mutable { a = 2; b = 5; };
add();
cout << a << " " << b << endl;

在这里插入图片描述

在这里插入图片描述

  1. 可以看到,a即使被mutable解除常性也无法影响实参,而b引用捕捉却可以。
3. lamba的原理
  1. lamba表达式的底层是一个仿函数类。
  2. 这个仿函数的类名是编译器根据特定规则生成的,保证不同lambda生成的类名不同。lambda表达式捕捉列表去捕捉变量本质就是生成这个lambda类的成员变量,也就是说捕捉列表里的变量会作为这个匿名类的成员变量,并在创建lambda对象时通过构造函数进行初始化。对于传值捕捉,这些成员变量是外部变量的副本;对于传引用捕捉,这些成员变量是引用。这也能完全解释为何传值无法影响到外层作用域的变量(因为操作的是副本);而后面的参数列表,返回值和函数体则对应lambda类中operator()的重载。

代码语言:javascript

AI代码解释

auto add = [a, &b]()mutable { a = 2; b = 5; };

class 类名//类名由编译器生成
{
public:
	类名(int a, int& b)
		:_a(a)
		,_b(b)
	{}

	void operator()()//如果没有mutable,后面加上const
	{
		_a = 2;
		_b = 5;
	}
private:
	int _a;
	int& _b;
};
4. 应用
  1. 在学习lambda表达式之前,我们能够使用的可调用对象只有函数指针和仿函数对象,函数指针的定义和定义仿函数(定义一个类)都比较麻烦。
  2. 而有了lambda表达式之后,功能和仿函数一样,用它去定义,简单又方便。

二、包装器

1. function

在这里插入图片描述

在这里插入图片描述

  1. std::function是一个类模板,也是一个包装器。它的实例对象可以包装其他的可调用对象,比如lambda表达式,函数指针、仿函数、bind表达式等。存储的可调用对象称作std::function的目标。如果std::function不含目标,则称它为空,空的std::function在使用时会抛异常。
  2. 函数指针、仿函数、lambda表达式、bind表达式等可调用对象的类型各不相同,而function则可以统一它们的类型(类型擦除),变成std::function类型。这样方便之后统一的存储和调用,比如存进容器里面。
  3. 使用:function<可调用对象返回类型(可调用对象参数类型)> 对象名称 = (&)可调用对象。对于类成员函数c++规定要指定类域并取地址
  4. 使用function和之后会讲到的bind需要包含头文件functional。

代码语言:javascript

AI代码解释

#include<functional>

int f(int a, int b)
{
	return a + b;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};

class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}

	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	//包装各种可调用对象
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	//包装静态成员函数,成员函数需要指定类域且前面加&才能获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	//不过静态成员函数前面可以不加&,但是推荐加
	function<int(int, int)> f5 = Plus::plusi;

	//包装普通类成员函数,不要忘了成员函数的第一个参数是this指针。
	Plus obj;
	//传递对象指针-最常用的方式
	function<double(Plus*, double, double)> f6 = &Plus::plusd;
	double r1 = f6(&obj, 1.1, 2.2);//传递地址
	//传递对象--有拷贝
	function<double(Plus, double, double)> f7 = &Plus::plusd;
	double r2 = f7(obj, 1.1, 2.2);
	//传递右值引用
	function<double(Plus&&, double, double)> f8 = &Plus::plusd;
	double r3 = f8(move(obj), 1.1, 2.2);
	//传递左值引用
	function<double(Plus&, double, double)> f9 = &Plus::plusd;
	double r4 = f9(obj, 1.1, 2.2);
	return 0;
}
  1. function包装类普通成员函数的第一个参数不是this指针类型,而是调用成员函数所需的一个媒介的类型,无论是对象指针,还是对象,对象引用都是一个调用成员函数的媒介,编译器通过这些媒介来获取实际的this指针
  2. 如果要包装类普通成员函数要将第一个参数加上const,那么对应的函数也要加上const进行修饰,权限不能放大。
2. bind
  1. bind是一个函数模板,同时它也是一个可调用对象的包装器。它会将接收的可调用对象进行一个再加工处理然后返回一个可调用对象,比如用来调整参数个数和参数顺序。bind也在<finctional>这个头文件中。
  2. 调用bind的一般形式是:auto newCallable = bind(callable, arg_list);其中newCallable是一个加工完成之后的可调用对象,arg_list是一个逗号分隔的参数列表,callable是要加工的可调用对象,这个newCallable的arg_list参数列表对应的就是callable的参数列表。
  3. 当我们调用newCallable时,newCallable会调用callable,然后将自己的参数列表arg_list传递给callable的参数列表
  4. arg_list中的参数一般包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示newCallable的参数。数值n表示的是生成的可调用对象中参数的位置。_1就是第一个需要传递给newCallable的参数,_2是第二个,以此类推。
  5. 改变顺序是改变像_1,_2这样的参数在参数列表中的位置,而调整参数个数是通过给定值来达到使用newCallable时就不用传递定值所在位置的实参,newCallable在传递参数给callable时会把这个定值当作实参之一。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐