【C++11】lambda表达式 包装器

文章目录

  • 1 lambda表达式
    • 1.1 引例
    • 1.2 lambda表达式的基本语法
    • 1.3 lambda表达式的底层原理
  • 2 包装器
  • 3 bind


1 lambda表达式

1.1 引例

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法:

#include 
#include 
int main()
{
	int array[] = { 4,1,8,5,3,7,0,9,2,6 };
	// 默认按照小于比较,排出来结果是升序
	std::sort(array, array + sizeof(array) / sizeof(array[0]));
	// 如果需要降序,需要改变元素的比较规则
	std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
	return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};
struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	return 0;
}

如果仿函数命名比较规范的话,像上面的命名方式的话那还好,如果遇到了像cmp1 cmp2 cmp3…这种命名方式而且还没有注释的话可以让人烦死,自己还得去找对应的源码实现,而如果在一个工程中有很多代码,找的代价也会比较大,所以C++11便新推出了一个语法就是lambda表达式。

1.2 lambda表达式的基本语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

lambda表达式各部分说明:

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体不能省略并且可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

我们可以来实现一个简单的add来验证一下:

int main()
{
	int x, y;
	cin >> x >> y;
	auto add = [=]()
	{
		return x + y;
	};
	cout << add() << endl;
	return 0;
}

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。像上面的add你甚至还可以这样写:cout<< [=](){return x + y;}()<< endl;

我们可以来看看mutable的应用场景,比如下面的代码:

int main()
{
	int x = 10,y = 20;
	auto swapInt = [=] {int tmp = x; x = y; y = tmp; };
	swapInt();
	return 0;
}

当我们编译时会直接报错的:
在这里插入图片描述
为什么呢?因为我们是用值捕捉的方式捕捉到的变量,而捕捉到的变量是一份拷贝,并且默认是不让你你修改的(可以理解为增加了const属性),所以当你修改变量是会直接报错的,那假如我们想让其修改呢?我们就可以用mutable(意思是易变的):
【C++11】lambda表达式 包装器_第1张图片

注意:

  1. 父作用域指包含lambda函数的语句块。
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
  4. 在块作用域以外的lambda函数捕捉列表必须为空。
  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  6. lambda表达式之间不能相互赋值,即使看起来类型相同。

前面的注意事项都很好理解,最后一个注意点我们可以验证下:

void (*PF)();
int main()
{
 auto f1 = []{cout << "hello world" << endl; };
 auto f2 = []{cout << "hello world" << endl; };

 //f1 = f2;   // 编译失败--->提示找不到operator=()
 // 允许使用一个lambda表达式拷贝构造一个新的副本
 auto f3(f2);
 f3();
 // 可以将lambda表达式赋值给相同类型的函数指针
 PF = f2;
 PF();
 return 0;
}

注意事项代码中都有注释。
至于为啥不允许赋值,我们后面讲解lambda表达式的原理时会给出解释。

1.3 lambda表达式的底层原理

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

我们写一段代码来验证一下:

class Rate
{
public:

	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}

private:
	double _rate;
};

int main()
{
	//  函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
	//  lamber
	auto r2 = [=](double monty, int year)->double { return monty * rate * year;};
	r2(10000, 2);
	return 0;
}

【C++11】lambda表达式 包装器_第2张图片从汇编的角度来看,我们不难发现lambda表达式在底层也是调用了operator来实现,那为什么lambda表达式不能够相互赋值呢?其本质是因为lambda表达式在底层的命名是采用uuid的方式生成唯一的类名,所以不同类型的对象自然不可以赋值了。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

那考考大家:lambda对象的大小是多少字节呢❓
答案其实已经显而易见了,由于lambda表达式的底层是用仿函数实现的,而仿函数是一个没有内置成员变量的类(空类),大小就是1字节喽,你回答对了吗?


2 包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

使用包装器前我们要引入头文件#include
类模板的原型如下:

// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

那包装器我们日常是如何使用的呢?

// 使用方法如下:
#include 
int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};

int main()
{
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;
	// 函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;
	// lamber表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)
	{return a + b; };
	cout << func3(1, 2) << endl;
	return 0;
}

我们可以用包装器来接受 函数指针 仿函数 lambda ,这样我们就可以用统一的类型来接受不同的参数,达到只实例化一份的目的。

但是在调用类中非静态成员函数(不包括仿函数)时要额外注意function的语法格式:
比如下面:

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	// 类的成员函数
	std::function<int(int, int)> func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;
	return 0;
}

我们知道静态成员函数是不包括this指针的,所以用之前的语法是没有问题的,但是由于成员函数有this指针,所以我们就要多给出一个额外的参数对象(我们一般喜欢给匿名对象来调用),通过参数对象来调用里面的成员函数。并且在指定类域是要加上&,这时语法的硬性规定。
【C++11】lambda表达式 包装器_第3张图片但是大家注意下面这种调用方式:
【C++11】lambda表达式 包装器_第4张图片我们也可以用对象指针来调用,但是这时候就不能够用匿名对象了,因为匿名对象是右值,是不能够&的,但是一般情况下我们不会选择这种方式。


3 bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。

一般来说,我们使用bind有下面这两种情况:

  • 1️⃣调换参数顺序
  • 2️⃣改变参数个数

其中调换参数顺序其实一般很少用到,而改变参数个数很有意思,我们接下来一个一个来看:
比如下面这段程序:

void Print(int x, int y)
{
	cout << x << ":" << y << endl;
}

假设我们不改变Print函数的实现,而打印结果时交换参数顺序,我们可以怎么做?
我们可以用bind来处理:

int main()
{
	int x = 10, y = 20;
	Print(x, y);
	auto RPrint = bind(Print, placeholders::_2, placeholders::_1);
	RPrint(x, y);
	return 0;
}

这里面的_1 _2 是什么鬼呀?这其实是封装在placeholders命名空间中的一个占位符,正如我们直接理解的那样, _1 _2 ……分别代表着第一个参数,第二个参数……,我们想要交换哪些参数的位置可以直接通过交换占位符的顺序即可。

交换参数顺序的用法其实比较鸡肋,我们平时一般也不怎么用到,但是改变参数个数的场景我觉得还是比较有意思的,我们接下来看看这种情况:

void mul(double x, double y)
{
	cout<< x * y<<endl;
}

struct fun
{

	fun(double rate)
		:_rate(rate)
	{}

	void mulR(double x, double y)
	{
		cout << x * y * _rate << endl;
	}

	double _rate;
};


int main()
{
	int x = 10, y = 20;
	function<void(double, double)> f1 = mul;
	function<void(double, double)> f2 = [=](double x,double y) {cout<< x * y<<endl; };
	return 0;
}

当我们要求使用跟上面参数一样的格式来接受fun中的mulR时我们直接写是会直接报错的,在上面我们讲解function时已经详细解释了原理,这里就不在多说了。那我们可以通过bind来处理:

function<void(double, double)> f3 = bind(&fun::mulR,f, placeholders::_1, placeholders::_2);

我们可以通过上面的方式来绑定处理,将第一个参数绑定写死,然后我们就可以只用两个参数的包装器来接受了,是不是很妙。当然,我们不仅可以绑死第一个参数,第二个三个n个参数我们都可以通过bind来绑死,值得注意的小细节是不论我们绑死的是第几个参数,我们其他没有被绑定的参数只能从_1不断变大


你可能感兴趣的:(C++进阶,c++,开发语言,lambda)