【C++】C++11引入的新特性(2)

当你无法从一楼蹦到三楼时,不要忘记走楼梯。要记住伟大的成功往往不是一蹴而就的,必须学会分解你的目标,逐步实施。💓💓💓

目录

  •✨说在前面

🍋知识点一:右值引用和移动语义(续)

•🌰1. 类型分类

•🌰2. 引用折叠

•🌰3. 完美转发

🍋知识点二:可变参数模板

•🌰1. 基本语法及原理

•🌰2. 包扩展

•🌰3. emplace系列接口

🍋知识点三:新的类功能

•🌰1. 默认的移动构造和移动赋值

•🌰2. 声明给缺省值

•🌰3. default和delete

•🌰4. final和override

🍋知识点四:STL中的一些变化

🍋知识点五:lambda

•🌰1. lambda表达式的语法

•🌰2. 捕捉列表

•🌰3. lambda的应用

•🌰4. lambda的原理

🍋知识点六:包装器

•🌰1. function

•🌰2. bind

 • ✨SumUp结语


  •✨说在前面

亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章我们开始给大家介绍了解和使用C++11所引入的特性。如果大家没有掌握好相关的知识,上一篇篇文章讲解地很详细,可以再回去看看,特别是旋转的部分,复习一下,再进入今天的内容。

C++11是一个大版本,更新了很多内容,这一篇文章我们继续给大家介绍C++11所引入的新特性。C++11这一个重大版本是必学的,如果大家准备好了,那就接着往下看吧~

   👇👇👇
💘💘💘知识连线时刻(直接点击即可)

【C++】C++11引入的新特性(1)

  🎉🎉🎉复习回顾🎉🎉🎉

        

 博主主页传送门愿天垂怜的博客

🍋知识点一:右值引用和移动语义(续)

•🌰1. 类型分类

1. C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)

2. 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。

3. 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,相当于类型转换,如move(x)、static_cast<X&&>(x)

4. 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值

5. 值类别 - cppreference.com 和 value_category 这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节。

6. 有名字,就是glvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是xvalu;没有名字,且可以被移动,则是prvalue。

 

•🌰2. 引用折叠

1. C++中不能直接定义引用的引用如 int& && r = i; 这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用

2. 通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用

3. 下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家需要一个一个仔细理解一下。

4. 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用

5. Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。

typedef int& lref;
typedef int&& rref;

//由于引用折叠,f实例化后总是左值引用
template<class T>
void f(T& x) {}

//由于引用折叠,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x) {}

int n = 0;

int main()
{
	//仅右值引用+右值引用才会折叠为右值引用
	lref& r1 = n;
	lref&& r2 = n;
	rref& r3 = n;
	rref&& r4 = 1;

	//没有折叠,实例化为void f(int& x)
	f<int>(n);
	//f<int>(1);

	//折叠,实例化为void f(int& x)
	f<int&>(n);
	//f<int&>(1);

	//折叠,实例化为void f(int& x)
	f<int&&>(n);
	//f<int&&>(1);

	//折叠,实例化为void f(const int& x)
	f<const int&>(n);
	f<const int&>(1);

	//折叠,实例化为void f(const int& x)
	f<const int&&>(n);
	f<const int&&>(1);

	//没有折叠,实例化为void f2(int&& x)
	//f2<int>(n);
	f2<int>(1);

	//折叠,实例化为void f2(int& x)
	f2<int&>(n);
	//f2<int&>(1);

	//折叠,实例化为void f2(int&& x)
	//f2<int&&>(n);
	f2<int&&>(1);

	return 0;
}
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;

	cout << &a << endl;
	cout << &x << endl;
	cout << typeid(x).name() << endl;
}

int main()
{
	//T被推导为int,实例化为void Function(int&& t)
	Function(10);

	int a;
	//T被推导为int&,实例化为void Function(int& t)
	Function(a);

	//move(a)为右值,T被推导为int,实例化为void Function(int&& t)
	Function(move(a));

	const int b = 8;
	//T被推导为const int&,实例化为void Function(const int& t)
	Function(b);

	//T被推导为const int,实例化为void Function(const int&& t)
	Function(move(b));

	return 0;
}

 

•🌰3. 完美转发

1. Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。

2. 但是结合我们之前的讲解,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这用我们想要保持t对象的属性,就需要使用完美转发实现。

3. 完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。

template<class T>
void Function(T&& t)
{
	Fun(forward<T>(t));
}

int main()
{
	//T被推导为int,实例化为void Function(int&& t)
	Function(10);

	int a;
	//T被推导为int&,实例化为void Function(int& t)
	Function(a);

	//move(a)为右值,T被推导为int,实例化为void Function(int&& t)
	Function(move(a));

	const int b = 8;
	//T被推导为const int&,实例化为void Function(const int& t)
	Function(b);

	//T被推导为const int,实例化为void Function(const int&& t)
	Function(move(b));

	return 0;
}

我们可以结合引用折叠、完美转发、移动构造和移动赋值,优化曾经写的list代码并使用list<string>进行测试:

 

🍋知识点二:可变参数模板

•🌰1. 基本语法及原理

1. C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数

• template <class ...Args> void Func(Args... args) {}

• template <class ...Args> void Func(Args&... args) {}

• template <class ...Args> void Func(Args&&... args) {}

2. 我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则

3. 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数

4. 这里我们可以使用sizeof...运算符去计算参数包中参数的个数

template<class ...Args>
void Print(Args&&... args)
{
	//计算参数包有几个参数
	cout << sizeof...(args) << endl;
}

int main()
{
	double x = 2.2;
	Print(); 
	Print(1); 
	Print(1, string("xxxxx")); 
	Print(1.1, string("xxxxx"), x);
	
	return 0;
}

总结:模板的原理是利用一个函数模板实例化出不同类型参数的函数,而可变参数模板的原理是利用可变参数模板实例化出多个不同参数个数的函数模板。

 

•🌰2. 包扩展

1. 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(...)来触发扩展操作。底层的实现细节如图1所示。

2. C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。

void showlist()
{
	//编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}
template<class T, class ...Args>
void showlist(T&& x, Args&&... args)
{
	cout << x << " ";
	showlist(args...);
}

template<class ...Args>
void Print(Args&&... args)
{
	//运行时
	/*if (sizeof...(args) == 0)
		return;*/
;
	//arg是N个参数的参数包,第一个传给x,剩下的传给第二个参数
	showlist(args...);
}

int main()
{
	double x = 2.2;
	Print(); 
    Print(1); 
    Print(1, string("xxxxx")); 
    Print(1.1, string("xxxxx"), x);
	
	return 0;
}

我们拿Print(1.1, string("xxxxx"), x)来举例看看发生了什么:

void showlist()
{
	cout << endl;
}
void showlist(double z)
{
	cout << z << endl;
	showlist();
}
void showlist(string y, double z)
{
	cout << y << " ";
	showlist(z);
}
void showlist(double x, string y, double z)
{
	cout << x << " ";
	showlist(y, z);
}

void Print(double x, string y, double z)
{
	showlist(x, y, z);
}
#endif

int main()
{
	double x = 2.2;
	Print(1.1, string("xxxxx"), x);
	
	return 0;
}

除了showlist之外,我们还有一种解析参数包的方法:

template<class ...Args>
void Arguments(Args ...args)
{}

template<class ...Args>
void Print(Args... args)
{
	//注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	Arguments(GetArg(args)...);
}

//上述Print等价于下列函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}

int main()
{
	double x = 2.2;
	Print(1.1, string("xxxxx"), x);
	
	return 0;
}

 

•🌰3. emplace系列接口

• template <class... Args> void emplace_back (Args&&... args);

• template <class... Args> iterator emplace (const_iterator position, Args&&... args);

1. C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。

2. emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列

3. 第二个程序中我们模拟实现了list的emplace和emplace_back接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。

4. 传递过程中,如果是Args&&... args的参数包,要用完美转发参数包,方式如std::forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。 

我们来看下面的代码示例,完整代码在这儿:

#include "String.h"
#include <list>

int main()
{
	list<bit::string> lt;
	//传左值,跟push_back一样,走拷贝构造
	bit::string s1("111111111111");
	lt.emplace_back(s1);
	cout << "*********************************" << endl;

	lt.push_back(s1);
	cout << "*********************************" << endl;

	//右值,跟push_back一样,走移动构造
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;

	lt.push_back(move(s1));
	cout << "*********************************" << endl;

	//直接传参,emplace_back不走隐式类型转换
	lt.emplace_back("111111111111");
	cout << "*********************************" << endl;

	lt.push_back("111111111111");
	cout << "*********************************" << endl;

	return 0;
}

我这里写了一个string存放在bit中,使得调用构造、拷贝构造、移动构造等可以显示打印出来,方便我们观察,大家可以在自己的环境中运行试试。 

也就是说,对于深拷贝的类型,emplace_back会少一个移动构造,比push_back快了一点;而对于浅拷贝的类型,emplace_back会比push_back少一个拷贝构造,更快了一些。所以总的来说,emplace系列接口比push和insert系列接口会更好用强大,推荐用emplace系列代替push和insert

#include <String.h>

int main()
{
	list<pair<bit::string, int>> lt1;
	pair<bit::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << "*********************************" << endl;

	lt1.emplace_back(move(kv));
	cout << "*********************************" << endl;

	//直接把构造pair参数包往下传,直接用pair参数包构造pair
	//这里达到的效果是push_back做不到的
	lt1.emplace_back("苹果", 1);
	cout << "*********************************" << endl;

	return 0;
}

 

🍋知识点三:新的类功能

•🌰1. 默认的移动构造和移动赋值

1. 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重 载/const取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器 会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载

2. 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造

3. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。

4. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
private:
	bit::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = move(s1);

	Person s2;
	Person s4;
	s4 = move(s2);

	return 0;
}

 

•🌰2. 声明给缺省值

这个其实在我之前更新的博客中是有说过的,忘记的可以去看这一篇博客:

【C++】深入理解类和对象(3)

 

•🌰3. default和delete

1. C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显式指定移动构造生成

2. 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

 

•🌰4. final和override

这个我们在继承和多态章节也说过了,可以去看这篇博客复习:【C++】面向对象之多态

 

🍋知识点四:STL中的一些变化

下图圈起来的就是STL中的新容器,但是实际最有用的是unordered_map和unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。

1. STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造和移动赋值,还有initializer_list版本的构造等,这些前面都讲过了,还有一些无关痛痒的需要时查查文档即可。

2. 容器的范围for遍历,这个在容器部分也讲过了。

 

🍋知识点五:lambda

•🌰1. lambda表达式的语法

1. lambda表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收lambda对象

2. lambda表达式的格式如下:

[capture-list] (parameters)-> return type { function boby }

•  [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,具体细节后面我们再细讲。捕捉列表为空也不能省略。

• (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略。

• ->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

//1、捕捉为空也不能省略
//2、参数为空可以省略
//3、返回值可以省略,可以通过返回对象自动推导
//4、函数体不能省略

auto func1 = []
	{
		cout << "hello bit" << endl;
		return 0;
	};

auto swap1 = [](int& x, int& y)
	{
		int temp = x;
		x = y;
		y = temp;
	};

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

	func1();

	int a = 0, b = 1;
	swap1(a, b);
	cout << a << ":" << b << endl;

	return 0;
}

 

•🌰2. 捕捉列表

1. lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉

2. 第一种捕捉方式是在捕捉列表中显式的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割[x,y,&z] 表示x和y值捕捉,z引用捕捉。

3. 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们lambda表达式中用了那些变量,编译器就会自动捕捉那些变量。

3. 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显式捕捉。[=, &x]表表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。

4. lambda 表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

5. 默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。

int x = 0;
//捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
	{
		x++;
	};
int main()
{
	//只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
		{
			//值捕捉的变量不能修改,引用捕捉的变量可以修改
			b++;
			int ret = a + b;
			return ret;
		};
	cout << func1() << endl;

	//隐式值捕捉
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};
	cout << func2() << endl;

	//隐式引用捕捉
	auto func3 = [&]
		{
			a++;
			c++;
			d++;
		};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;

	//混合捕捉
	auto func4 = [&, a, b]
		{
			//a++;
			//b++;
			c++;
			d++;
			return a + b + c + d;
		};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;

	auto func5 = [=, &a, &b]
		{
			a++;
			b++;
			/*c++;
			d++;*/
			return a + b + c + d;
		};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;
	//局部的静态和全局变量不能捕捉,也不需要捕捉
	static int m = 0;
	auto func6 = []
		{
			int ret = x + m;
			return ret;
		};
	//传值捕捉本质是一种拷⻉,并且被const修饰了
	//mutable相当于去掉const属性,可以修改了
	auto func7 = [=]()mutable
		{
			a++;
			b++;
			c++;
			d++;
			return a + b + c + d;
		};
	cout << func7() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}

 

•🌰3. lambda的应用

1. 在学习lambda表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦烦。使用lambda去定义可调用对象,既简单又方便

2. lambda在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。

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());
	//不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) 
		{
		return g1._price < g2._price;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) 
		{
		return g1._price > g2._price;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) 
		{
		return g1._evaluate < g2._evaluate;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
		{
		return g1._evaluate > g2._evaluate;
		});
	return 0;
}

 

•🌰4. lambda的原理

1. lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个lambda以后,编译器会生成⼀个对应的仿函数的类

2. 仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。

3. 上面的原理,我们可以透过汇编层了解一下,下⾯的汇编层代码印证了上面的原理。

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;
	//lambda
	auto r2 = [rate](double money, int year)
		{
			return money * rate * year;
		};
	//函数对象
	Rate r1(rate);
	r1(10000, 2);
	r2(10000, 2);
	auto func1 = [] 
		{
			cout << "hello world" << endl;
		};
	func1();
	return 0;
}

底层汇编代码如下: 

可以发现本质都是在调用operator()。r1是仿函数对象,自然是调用operator(),而lambda也调用operator()就说明了它的本质其实也是仿函数,它的类名是编译器根据一定规则生成的。 

 

🍋知识点六:包装器

•🌰1. function

1. std::function是一个类模板,也是一个包装器std::function的实例对象可以包装存储其他的可以调用对象,其中包括函数指针、仿函数、lambda、bind表达式等等,存储的可调用对象被称为std::function的目标。若std::function不含目标标,则称它为空。调用空std::function 的目标导致抛出bad_function_calll异常。

2. 以上是 function的原型,他被定义<functional>头文件中。functional/function是function的官方文件链接。

3. 函数指针、仿函数、 lambda等可调用对象的类型各不相同,std::function的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型,下面的第二个代码样例展示了std::function作为map的参数,实现字符串和可调用对象的映射表功能。

#include <functional>

#if 1
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; };

	cout << f1(1, 2) << endl;
	cout << f2(1, 2) << endl;
	cout << f3(1, 2) << endl;

	//包装静态成员函数,需要指定类域,&才可以获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(2, 3) << endl;

	//普通成员函数,隐含this指针
	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus pd;
	cout << f5(&pd, 2.2, 3.3) << endl;

	function<double(Plus, double, double)> f6 = &Plus::plusd;
	cout << f6(pd, 1.1, 1.1) << endl;

	function<double(Plus&&, double, double)> f7 = &Plus::plusd;
	cout << f7(move(pd), 1.1, 1.1) << endl;
	cout << f7(Plus(), 1.1, 1.1) << endl;

	return 0;
}

150. 逆波兰表达式求值 - 力扣(LeetCode)

//传统方式的实现
class Solution {
public:
	int evalRPN(vector<string>& tokens) {
		stack<int> st;
		for (auto& str : tokens)
		{
			if (str == "+" || str == "-" || str == "*" || str == "/")
			{
				int right = st.top();
				st.pop();
				int left = st.top();
				st.pop();
				switch (str[0])
				{
					case '+':
						st.push(left + right);
						break;
					case '-':
						st.push(left - right);
						break;
					case '*':
						st.push(left * right);
						break;
					case '/':
						st.push(left / right);
						break;
					}
				}
			else
			{
				st.push(stoi(str));
			}
		}
		return st.top();
	}
};
//使用map映射string和function的⽅式实现
//这种方式的最大优势之一是方便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution {
public:
	int evalRPN(vector<string>& tokens) {
		stack<int> st;
		//function作为map的映射可调用对象的类型
		map<string, function<int(int, int)>> opFuncMap = {
		{"+", [](int x, int y) {return x + y; }},
		{"-", [](int x, int y) {return x - y; }},
		{"*", [](int x, int y) {return x * y; }},
		{"/", [](int x, int y) {return x / y; }}
		};
			for (auto& str : tokens)
			{
				if (opFuncMap.count(str)) //操作符
				{
					int right = st.top();
					st.pop();
					int left = st.top();
					st.pop();
					int ret = opFuncMap[str](left, right);
					st.push(ret);
				}
				else
				{
					st.push(stoi(str));
				}
			}
		return st.top();
	}
};

 

•🌰2. bind

1. bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器对接收的fn可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数顺序。bind 也在<functional>这个头文件中。

2. 调用bind的一般形式: auto newCallable = bind(callable,arg_list); 其中newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们去调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

3. arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示的是newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的一个命名空间中

using placeholders::_1;
using placeholders::_2;

int Sub(const int& a, const int& b)
{
	return (a - b) * 10;
}

int main()
{
	auto sub1 = bind(Sub, _1, _2);
	cout << sub1(10, 5) << endl;

	auto sub2 = bind(Sub, _2, _1);
	cout << sub2(10, 5) << endl;

	auto sub3 = bind(Sub, 100, _1);
	cout << sub3(30) << endl;

	auto sub4 = bind(Sub, _1, 100);
	cout << sub4(30) << endl;

	function<double(double, double)>  f = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f(1.1, 1.1) << endl;

	return 0;
}

年化利率理财计算模型:

int main()
{
	 //计算复利的lambda
	auto func1 = [](double rate, double money, int year)->double {
		double ret = money;
		for (int i = 0; i < year; i++)
		{
			ret += ret * rate;
		}
		return ret - money;
	};
	

	//绑死一些参数,实现出支持不同年化利率,不同金额和不同年份计算出复利的结算利息
	function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
	function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
	function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
	function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);

	cout << func3_1_5(1000000) << endl;
	cout << func5_1_5(1000000) << endl;
	cout << func10_2_5(1000000) << endl;
	cout << func20_3_5(1000000) << endl;

	return 0;
}

   

 • ✨SumUp结语

到这里本篇文章的内容就结束了,本节给大家继续讲解了C++11新增的一些内容:引用、可变模板参数、lambda等等。希望大家能够认真学习,打好基础,迎接接下来的挑战,期待大家继续捧场~💖💖💖

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值