别在学你的C++98了,程序员需要跟上时代,快来学C++11,30000字把C++11的常用特性一次性讲清

在这里插入图片描述

列表初始化

在C++98的时候支持了大括号{}对数组、结构体进行了列表初始化

struct test
{
	int _age;
	char _a;
};

int main()
{
	int arr[] = { 1, 2, 3, 4 };
	struct test t1 = { 10, 'a' };
	return 0;
}

简单使用

C++11后,对这个列表初始化的功能进行了拓展,让内置类型,自定义类型,都能够使用大括号进行初始化,并称之为聚合初始化
注意:列表初始化这里需要和构造函数的初始化列表区分开来,是两个完全不一样的东西

struct Point
{
	Point(int x, int y)
		:_x(x) ,_y(y)
	{}
	int _x;
	int _y;
}
int main()
{
	//一切皆可用{}初始化,并且可以不写=
	int x = { 2 };//对于内置类型,可以用大括号初始化
	int z{ 3 };//内置类型可以去掉等号
	int arr[] { 1, 2, 3 };//数组也可以去掉等号

	Point p2(1, 2);//正常初始化
	Point p1 = { 1, 1 };//大括号赋值
	Point p3{2 , 2};//去掉赋值

	return 0;
}

这里展示的去掉赋值符号的写法,只能说知道有这种语法,其他人用的时候自己能够看懂,可以不用的,并且对于后面这种自定义类型,p2 p3都是会调用构造函数的,逻辑都是相同的,并无本质区别

这种写法在对于new自定义类型的时候会有一定优势

struct Point
{
	Point(int x, int y)
		:_x(x) ,_y(y)
	{}
	int _x;
	int _y;
}
int main()
{
	Point* p = new Point[2]{ { 1, 2 }, { 2, 3 } };//就不需要用匿名结构体,或者先定义两个变量,然后初始化
	return 0;
}

其实对于这种任意大括号的类型,能够进行初始化,也是因为C++11支持了多参数构造函数的隐式类型转换,在C++98的时候,只是支持单参数的隐式类型转换,我们可以在构造函数前加explicit关键字,禁止单参数或者多参数的隐式类型转换

class A
{
	explicit A(int x, int y)
		:_x(x)
		,_y(y)
	{}
	int _x;
	int _y;
};

int main()
{
	string str = "hello";//单参数的隐式类型转换

	A a1(1, 2);//right,多参数构造函数的隐式类型转换
	A a2 = {1, 2};//err
	return 0;
}

这种初始化方式和普通初始化方式都会生成临时对象,临时对象具有常性,所以对于引用来说,一样会涉及到权限问题

class A
{
	A(int x, int y)
		:_x(x)
		,_y(y)
	{}
	int _x;
	int _y;
};

int main()
{
	A& a1 = { 1, 2 };//err 权限放大
	const A& a2 = {1, 2};//right 权限平移
	
	return 0;
}

initializer_list

需要注意的是:数据容器中用大括号初始化,并不是使用的C++11的列表初始化,而是调用的C++11出现的一个构造函数,这个构造函数使用了一种针对列表初始化的新类型,我们可以通过auto接收,typeid打印查看查看

int main()
{
	vector<int> v1 = { 1, 2, 3 };
	vector<int> v2 = { 1, 2, 3, 4, 5 };
	
	auto init = { 1, 2, 3 };
	cout << typeid(init).name() << endl;//initializer_list
	return 0;
}

在这里插入图片描述
根据它的类型的写法,我们可以初步判断这个initializer_list是一个模板类
在这里插入图片描述在这里插入图片描述
所以只要是支持了C++11的编译器,这种用大括号括起来的东西,都都能够识别成initializer_list,这是因为这些{ 10, 20, 30 }都是常量数组,是放在常量区的,在initializer_list中会有一个_start指针指向这个数组在常量区的首地址,另一个_finish指针指向结束位置的下一个位置(这里具体的识别结束位,起始位,编译器会帮忙支持)
所以我们用{}初始化的方式,本质上是调用了initializer_list的构造函数

这种实现方式,使得只要是用{}初始化的,必须是数组,不能是指针(字符串比较特殊,可以支持)

int main()
{
	int* p1 = { 1, 2 ,3 };//err
	const int* p2 = { 1, 2, 3 };//err
	
	const int* arr = new int[3]{1, 2, 3};//right,这个大括号是作用的int[3],还是作用的数组
	const char* str = "hello";//right
	return 0;
}

我们再次思考vector数据容器是如何支持大括号的初始化的
在这里插入图片描述
这里std库的vector的构造函数支持了一个initializer_list< value_type > il的构造函数,构造函数中通过一个范围for拿到大括号中每一个数据,然后通过一个push_back()插入数据即可实现,或者也可以在该构造函数中复用一个迭代器区间的构造函数也可以简单的支持

所以我们此时就能明白,为什么前面的例子中Point结构体在用{}初始化的时候,其参数个数不能是任意多个,同理我们可以给他支持一个initializer_list版本的构造函数

class Point
{
public:
	Point(initializer_list<int> il)
	{
		_x = *il.begin();
		_y = *il.begin() + 2;
	}

	int _x;
	int _y;
};
void test2()
{
	Point p = { 1, 2, 3, 4, 5, 6 };
	cout << "p._x = " << p._x << " ";
	cout << "p._y = " << p._y << endl;
}

在这里插入图片描述

除了vector其他的数据容器也都支持了initializer_list,也都可以用大括号进行初始化,需要注意的是比如map的initializer_list< value_type >的value_type是pair类型,所以对于map类型需要对每一个kv都给一个pair的构造,这里我又用了一个{},这里对pair类型的初始化,就类似于上面的Point那样,也就是多参数的隐式类型转换,当然也可以对每一个pair(first, second)-匿名构造 或者 make_pair(first, second)-函数调用

int main()
{
	map<string, string> Mymap = { { "insert", "插入" }, { "sub", "删除" } };
	return 0;
}

甚至实现了赋值重载的数据容器,在C++11中也支持了initializer_list的赋值重载

map<string, int> m;
m = {{"apple", 1}, {"banana", 2}, {"cherry", 3}};

auto

auto能够自动识别类型,包括变量类型、函数类型,比较方便的定义函数类型,类型名很长的变量,比如类模板等

decltype

我们前面说了typeid可以拿到类型名,但是只能用于打印,而真正想要拿到类型,并且用这个类型创建新变量的,就需要decltype关键字了,这个关键字有的时候能解决auto解决不了的情况,decltype可以推出对象的类型,再定义变量,或者作为模板实参,甚至可以推导表达式类型(表达式的结果类型)

class A
{
private:
	decltype(malloc) pf;//定义一个和malloc相同类型的变量	
	auto pf1 = malloc;//err
};
template<class T>
class B
{
	//use A
};
test()
{
	A a;
	B<decltype(a)> pfb;

	decltype(3 * 5.0) b;//定义的类型是3 * 5.0的结果类型double
}

nullptr

在C++中我们将NULL定义为了0,0既能是空指针,也可以是整型常量,那么我们在 int* p = NULL;的时候实际上是两步,第一步NULL宏替换为0,然后隐式类型转换成int指针类型,所以C++11中出现了nullptr,而nullptr只能代表空指针((void) 0) ,这样就会更精确,以免不必要的错误
在这里插入图片描述
这张图是库中的实现,对于C,NULL被定义为了((void*)0),但是在C++中,又被换成了0,所以C++11这是为了填坑,自己埋的。

void fun(int x)
{}
void fun(int* p)
{}
int main()
{
	fun(NULL);//匹配了第一个
	return 0;
}

STL的变化

新容器

array、forward_list、unordered_map、unordered_set
forward_list是最简洁的单链表,很鸡肋,别用,array也是垃圾,别用,完全可以用vector替代,简单的情况完全可以就用普通数组,相比于普通数组唯一的优势就是越界检查更严格

void test()
{
	array<10> arr1;
	int arr2[10];

	arr1[15];//指针解引用
	arr2[15]/;//调用operator[]
}

C++11中引入hash实现的unordered_map、unordered_set很有用,不要求有序的时候,unordered_map unordered_set系列比map、set的效率更低

新接口

迭代器

在这里插入图片描述
cbegin、cend…这个c就是const,也没有很必要的场景,完全可以直接用const去修饰非const和const容器的迭代器,略过

初始化 赋值

也就是最开头的initializer_list

emplace系列

在这里插入图片描述
emplace的出现还对push_back这些进行了升级,有了很大的性能提升,这和后面的右值引用,模板的可变参数有关
在这里插入图片描述
并且所有的容器还新增了移动构造和移动赋值,大幅度提高了深拷贝的性能

默认成员函数

原来是六个默认构造,现在新增了两个,一个移动构造,一个移动赋值
这俩在出现深拷贝的情况就需要我们自己实现、浅拷贝的类不需要我们实现

问题

我们不写,一定会自动生成吗?我们不写,自动生成的移动构造和移动赋值会做什么?
不一定,如果我们没有实现析构函数、拷贝构造、赋值重载,那么编译器会自动生成一个默认的移动构造,默认生成的移动构造,对于内置类型会逐字节拷贝,对于自定义类型,如果实现了移动构造,则调用移动构造, 否则调用拷贝构造,移动赋值也完全相同

这样实现是因为,涉及到深拷贝的类,都会自己实现拷贝构造、赋值重载,并用析构来销毁动态空间,只要是这种情况,也就需要我们自己实现移动构造,所以这四个函数,一般要么是都不写,都默认生成,要么是都需要我们自己实现

C++11我们也有关键字可以强制编译器生成默认的函数(八个默认函数都可以强制生成),比如我们生成了拷贝构造,就不会生成移动构造,我们就只需要:写一个移动构造的声明,然后在声明后面加上 = default即可,如下例:

class A
{
public:
	A(const char* name = "", int age = 20)
		:_name(name)
		, _age(age)
	{}
 	A(const A& p)
 		:_name(p._name)
 		,_age(p._age)
 	{}
	A(A&& p) = default;
private:
	string _name;
	int _age;
};

右值引用和移动语义

左值和右值

在赋值符号的左边,在赋值符号的右边区分左右值并不完全准确

左值是变量名或解引用,我们能够获取其地址,如果是非const的左值,还能对其赋值;左值可以出现在赋值符号的左边和右边

void test()
{
	//"hello" str a1 a2 p *p pa *pa这些都是左值

	int a1;
	const int a2;
	int* p = new int;
	int* pa = nullptr;

	"hello";//字符串比较特殊,但是提出来,"hello" 实际是首元素地址
	cout << &("hello") << endl;
	const char* str = "hello";;//str能够拿到"hello",也是因为"hello"可以看着是首元素地
}

右值只能出现在赋值符号的右边,一般是数据的表达式字面常量、表达式返回值,函数返回值(不能是左值引用返回),右值无法取地址

int min(int x, int y){return x > y;}
void test()
{
	int x = 10;
	int y = 20;
	//10  x + y  min(x, y)就是右值
	10;
	x + y;
	int ret = min(x, y);//min(x, y)实质指的是函数返回值的临时拷贝
}

最根本的区别:是否可以取地址

左值引用 右值引用

左值引用就是对左值的引用对左值取别名,右值引用就是对右值的引用对右值取别名

我们之前的引用都是左值引用,对左值取别名

//这里的rp rpb rc pvalue都是
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

const左值引用可以给右值取别名,非const左值不行

const int& r2 = 10;

template<class T>
void func(const T& x);//我们这里的const的一个作用就是既能引用左值,也能引用右值

func(10);//调用接收右值

左值引用的使用场景:1、做参数2、做返回值 价值:减少拷贝

右值引用是&&,是对右值取别名

//rr1 rr2 rr3就是右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = min(x, y);

右值引用不做处理不能给左值取别名,用move函数处理了左值,就能够让右值引用给左值取别名

int a;
int&& rr1 = a;//err
int&& rr2 = move(a);//后面讲move

右值引用的使用场景,如下例所示

//这里的返回值不能是string& 返回局部对象的引用,外部拿到的是野引用了,也不会有任何的优化
string func()
{
	string str;
	//...
	return str;//这里的str是局部变量,局部变量在离开作用域时
	//会随着函数销毁栈帧调用自身的析构函数,所以外部拿到的func的返回值也是野的,非法的
}


 
int main()
{
	string ret = func();//只要是连续的构造/拷贝构造,并且是传值返回,编译器会做优化,和二为一。
	//未优化之前是func中的str在返回之前先拷贝构造创建一个临时对象,然后再次把这个临时对象拷贝构造给ret(下图是初始化阶段的赋值重载,这会被解析成拷贝构造)
	//编译器做优化,即把这两个步骤合二为一,func的str直接拷贝构造给到ret

	//简单来说就是优化前:两次拷贝构造,包含两次深拷贝,优化后:一次拷贝构造,包含一次深拷贝
	return 0;
}

在这里插入图片描述

插入一个理解:
1、这俩构成函数重载吗?构成,实参是左值走上面,实参是右值走下面

void func(int& r)//只能接收左值
{
	cout << "int& r" << endl;
}
void func(int&& r)//只能接收右值
{
	cout << "int&& r" << endl;
}

2、加上const,如果实参是右值?这俩会出现冲突吗?不会,优先使用int&& r,对于接收右值,const int& r是备胎,int&& r是正宫,没有int&& r时,备胎就能上位

void func(const int& r)//能接收左值和右值
{
	cout << "const int& r" << endl;
}
void func(int&& r)//只能接收右值,相比于const int& r 更能匹配右值
{
	cout << "int&& r" << endl;
}

回到传值返回的问题,优化前,这个过程总共会涉及到三块动态开辟出来的空间
在这里插入图片描述
优化后也还是会再开辟一块空间,然后把局部的str给delete掉
在这里插入图片描述
所以这里我们可以用右值引用来处理
首先我们先再加一个概念,我们将内置类型的右值称为纯右值,自定义类型的右值称为将亡值
自定义类型叫做将亡值可以理解:对于自定义类型的运算,函数调用返回值这些情况,其结果都是临时的,所以我们就称其为将亡值

string s1, s2;
s1 + s2;//运算符重载,s1+s2在内部计算的时候都会开辟新的空间,开辟的空间在离开这个运算符重载函数后也会销毁
string newstring = to_string(123);//to_string内部也会开辟空间来处理这个123,同理会销毁这块空间,编译器会把这里优化成一次深拷贝

1、如果是string ret = 左值,只能老老实实拷贝,因为ret和这个左值都需要

2、而string ret = 右值将亡值,我们是否能够利用将亡的那块空间,不进行这一次的深拷贝呢?

移动语义

移动语义:移动语义就是利用右值引用,实现针对右值的构造和针对右值的赋值,简称:移动构造和移动赋值

我们下面针对移动拷贝进行讲解,移动赋值的思想是类似的
移动拷贝就是利用将亡值的动态开辟的空间进行的

我们通过观察内存情况来理解

//这部分是我们自己实现的string的一部分函数,也是我们想要观察的部分
namespace my_string
{
	class my_string
	{
		void swap(string& s)
		{
			swap(_str, s._str);
			swap(_size, s._size);
			swap(_capacity, s._capacity);
		}
		//构造...
		
		//拷贝构造...
		
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		string& operator=(string&& s)
		{
			cout << "string& operator=(string && s) -- 移动拷贝" << endl;
			swap(s);
			return *this;
		}
	};
}
my_string::string func()
{
	my_string::string str("HHEXXXXXXXXXXXXXXXXXXXXXXXX");
	//...
	return str;
}

int main()
{
	my_string::string ret;
	ret = func();
	return 0;
}

这里我们在func函数中创建了一个string类型的变量str,在返回之前的_str的地址是0x01596810
在这里插入图片描述
这里我们用了右值引用,所以s就是外面的string类型的变量str的别名,所以动态开辟的_str字符串都是同一个地址
在这里插入图片描述
此时精髓就来了,我们不会再开辟一块新的动态内存,而是直接让this的_str _size _capacity和s的_str _size _capacity交换
在这里插入图片描述
然后之前func创建的变量str依旧会销毁,但是此时销毁的实际上是原来ret申请的内存空间,因为我们这里本来就要赋值给ret,ret原来的值已经不重要了,所以直接偷梁换柱,完成了移动拷贝,整个过程没有new新的动态内存空间,对于像map、set这种对于拷贝消耗巨大的数据容器,这种方式的收益巨大!

上面这个例子是把初始化和赋值分开了,所以不是连续的构造、拷贝构造,所以不会进行优化(my_string::string ret; ret = func()😉
我们下面考虑my_string::string ret = func();

```cpp
my_string::string func()
{
	my_string::string str("HHEXXXXXXXXXXXXXXXXXXXXXXXX");
	//...
	return str;
}

int main()
{
	my_string::string ret = func();
	return 0;
}

因为是连续的构造+拷贝构造,所以会优化成一个拷贝构造,也就是直接把str拷贝给ret,str在func中是左值,所以理应只能调用拷贝构造,去进行深拷贝,但是这种情况非常多,所以编译器对其进行了特殊处理,也就是第二次优化,因为str作为了返回值,又是局部变量,出了作用域就销毁了,所以str是符合将亡值的特点的,编译器会把str识别成右值将亡值(类似于编译器帮我们move了一下),此时就能够调用移动构造

我们回顾my_string::string ret; ret = func(); 这种情况虽然还是会生成临时对象,但是第一步生成临时对象的过程,编译器也会把str特殊处理优化为右值将亡值,使得这个过程是移动构造,整体就是两次移动构造

move

move是一个函数,比较复杂,我们只需要知道:这个函数的输入是一个左值,输出的返回值是实参的右值将亡值

string ret1("XXX");
move(ret1);//不会对ret1有影响
string ret2 = ret1;//也不会对ret1有影响
string ret3 = move(ret1);//此时ret1被释放了,因为这里ret1会作为右值将亡值,ret1把ret3不要的空间带走了

再看一个例子

int main()
{
	list<my_string::string> lt;
	my_string::string s1("hello");
	lt.push_back(s1);

	cout << endl << endl;
	my_string::string s2("XXXXXXXX");
	lt.push_back(move(s2));
	lt.push_back("YYYYYY");
	return 0;
}

这里对于 lt.push_back(s1);
第一步会调用构造,构造中new一个Node结点,然后new Node(val)的时候,val是string,又会调用string的拷贝构造,又会new一块string的空间

所以在C++11中,我们用move把s2变为右值,或者直接传递的是右值"YYYYYY",都会调用移动构造版本的push_back
在这里插入图片描述
在这里插入图片描述
所以对于C++11出现了右值引用之后,我们对于传参过程,就可以区分左值和右值,对于左值就去调用拷贝构造,进行深拷贝;对于右值就去调用移动构造,进行资源转移

除了对于拷贝构造、赋值重载,使用插入接口,如果插入的值是右值,也可以用右值引用来进行资源转移操作,也可以大大提高效率,STL库中在C++11后,所有的插入接口,都提供了右值版本
在这里插入图片描述

C++11中还更新了另一批接口emplace系列,这个我们放在后面讲

tips

1、左值引用的核心价值是减少拷贝,提高效率
2、右值引用的核心价值是进一步减少拷贝,弥补左值引用没有解决的场景,比如:自定义类型涉及到深拷贝的传值返回,进一步提高效率
3、浅拷贝的类没有必要实现移动构造,传值返回拷贝构造的代价不大
4、这两次的编译器优化是所有编译器都支持的,是C++11标准强制要求的

完美转发

万能引用

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

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

int main()
{

	PerfectForward(10); //右值

	int a;
	PerfectForward(a);	//左值
	PerfectForward(move(a));	//右值
	
	const int b = 8;
	PerfectForward(b);		//const 左值
	PerfectForward(move(b));	//const右值
	return 0;
}

提问:PerfectForward是T&& t类型,能够接收右值,但是能够接收左值吗?

对于普通函数,左值只能接收左值,const左值能够接收左值和右值,右值和const右值只能接收右值
但是这里是模板,模板中的&&修饰的模板类型,不是右值引用,而是万能引用:既可以接收左值,也可以接收右值

从理解上:模板类型不是具体的类型,是通过实参的类型而构造多个函数,所以如果实参是右值,则会构建右值引用版本,如果实参是左值,则会构建左值引用版本

这里我们引入教材上的名词:万能引用中,实参如果是左值,我们称为:引用折叠,形象的理解就是实参是左值,我们的万能引用的&&就折叠成&,也就是左值引用版本了

万能引用还需要注意一个点,参数的类型是模板类型,这个模板类型是推导出来的,而不是已经确定了的,这个问题会出现在类是模板类,但是其中的某个函数,这个函数的参数类型是T模板类型,这种情况下,看着这个函数是模板函数,但是当我们在定义对象的时候,类中的函数的参数类型,就已经确定了,所以此时就不是万能引用了,如下例所示:

template<class T>//修饰的是类
class A
{
public:
	A(){}
	void Fun(T&& x)//在对象创建之后,这里的T就已经是固定的类型了,并不能触发万能引用
	{
		cout << "void Fun(T&& x)" << endl;
	}
	//void Fun(const T& x)
	//{
	//	cout << "void Fun(const T& x)" << endl;
	//}


	//解决办法,我们对类模板中的成员函数改写为模板函数,此时调用时编译器才会确定函数类型(这种方式不需要我们手动再去决定函数类型,编译器通过参数类型自动推导)
	// template<class Ty>
	// void Fun(Ty&& x)
	// {}

};
int main()
{
	A<int> a1;
	int x = 10;
	a1.Fun(x);//err void Fun(const T& x)可以接收
	a1.Fun(10);// void Fun(const T& x) void Fun(T&& x)可以接收
	return 0;
}

完美转发的始与终

回到主线,我们这里运行起来的结果如下图在这里插入图片描述
发现都调用的是左值引用,我们换成普通函数测试

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
void PerfectForward(int&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10); //右值
	
	int a;
	PerfectForward(move(a));	//右值
	return 0;
}

在这里插入图片描述
单纯的右值引用函数,其内部调用Fun(),依旧执行的是void Fun(int& x)

为什么?
我们先来思考一下:
r是左值吗?是
rr是左值吗?是

int a;
int& r = a;
int&& rr = move(a);
cout << &r << endl;
cout << &rr << enld;

都能够进行取地址,所以都是左值
在这里插入图片描述
我们上面讲过,右值是不能取地址的,除了不能取地址,还有一个特点:右值是不能修改的

int a;
int& r = a;
int&& rr = move(a);
cout << &r << endl;
cout << &rr << enld;
r++;
rr++;

所以印证了,r和rr都是左值,我们回顾前面的移动语义以及各种插入的右值版本的情况,其实现都是利用的资源转移
比如,Fun(string&& str) { //… swap(str);} 来进行的,所以变量str不是右值,而是左值,并且我们调用swap函数的时候形参是string& str,只有左值才能调用到这个swap,所以也印证了str本身是左值

其实这也是C++11为了提高性能,提高性能,强行特殊规定的,右值引用的属性会被编译器识别成左值,否则在移动语义等场景下,无法完成资源转移

此时我们再去看完美转发这里的PerfectForward函数的t变量,t的属性是左值,所以Fun(t)就都调用了左值引用

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

所以我们如果在Fun中还要右值引用,那么就需要用move,但这种方式过于生猛,让Fun这一层接收到的t全都是右值,这并不能满足我们的需求——左值对应左值引用,右值对应右值引用,所以终于到了这一部分的核心——完美转发forward模板函数
在这里插入图片描述
forward这里原型看着会比较麻烦,只需要知道forward< T >(x) 也就是对T类型的x进行完美转换

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<class T>
void PerfectForward(T&& t)//万能引用
{
	Fun(forward<T>(t));//完美转发
}

int main()
{

	PerfectForward(10); //右值

	int a;
	PerfectForward(a);	//左值
	PerfectForward(move(a));	//右值
	
	const int b = 8;
	PerfectForward(b);		//const 左值
	PerfectForward(move(b));	//const右值

	return 0;
}

在这里插入图片描述
在STL的构造、拷贝构造、赋值重载、插入操作中,都会涉及到右值引用,如果想要前往深层的同时保持右值属性,那么就需要在每一层传参的时候都带上forward< T >()完美转发

可变模板参数

可变参数

可变参数我们自己一般不会写,但是为了后续学习,我们还是需要了解
我们之前用的printf scanf都是可变参数的函数
在这里插入图片描述
这里的三个点,就代表了这里是可变参数,表示能够写很多参数,其底层会用一个数组,把可变参数存起来,一个元素就是一个参数,printf通过访问数组,就把对应的值取出来,就是对应的参数了

可变参数模板

C++11支持了可以接受可变参数的函数模板和类模板,通常命名为 template< class … Args >,当然可以取其他名字,但是大多数都固定取Args,就类似于我们普通模板参数会用class T,class K,都是习惯罢了
这个Args是一个模板参数包,包含零到任意个模板参数,也就是零到任意个模板类型;args是模板类型对应的函数形参参数包,也就是变量

template<class ... Args>
void showlist(Args... args)//template中定义了几个类型,这个args就表示多少个形参
{}

另:
1、模板参数:对于函数模板——其类型是推演出来的
2、对于类模板——其类型是显示实例化出来的
3、模板函数、模板类的生成都是在编译阶段完成的

举例理解

template<class ... Args>
void showlist(Args... args)//template中定义了几个类型,这个args就表示多少个形参
{}
int main()
{
	ShowList(1);
	ShowList(1.2, string("hello"));
	ShowList(1, 2.1, vector(), map<int, string>());
	return 0;
}

我们对于这个ShowList模板函数,可以通过sizeof拿到参数包的参数个数,但是语法很奇怪

template< class T, class ... Args >
void ShowList(T value, Args ... args)
{
	cout << sizeof...(args) << endl;
}

我们想要访问到每一个参数,不能通过args[i]的方式访问,必须上下文扩展参数包,进行参数推演

template<class T>//这个模板是ShowList(T val, Args ... args)的结束条件
void ShowList(T val)
{
	cout << val << " ";
	cout << endl;
}
template<class T, class ... Args>
void ShowList(T val, Args ... args)
{
	cout << val << " ";
	ShowList(args...);
}
int main()
{
	//打印过程:
	//T val拿到'a', args拿到'a'后面的所有实参
	//内部传参ShowList(args...),此时下一层的class T就是2,然后继续递归
	//1.5是T val,此时args...只包含了一个参数,string("hello"),此时就会取到第一个函数模板
	//然后执行cout << val << " ";cout << endl;结束打印
	//这是一种编译时递归
	ShowList('a', 2, 1.5, string("hello"));
}

上面这种方式是通过class T辅助操作,我们也可以不用class T,只用class …Args,只需要在外面套一层,再加上一个支持无参的结束函数

_ShowList()
{
	cout << endl;
}
template<class T, class ... Args>
void _ShowList(T val, Args ... args)
{
	cout << val << " ";
	_ShowList(args...);
}
template<class ... Args>
void ShowList(Args ... args)
{
	_ShowList(args...);
}
int main()
{
	ShowList(1);
	ShowList(1, 21.2);
	
	return 0;
}

除了上面这种方式,我们还有下面这种方式

void ShowList()
{
	cout << endl;
}
template<class T>
void PrintArg(T t)
{
	cout << t << ' ';
}
template<class ...Args>
void ShowList(Args ... args)
{
	//这行代码的语法为,编译器通过 ... 检测args参数表中的参数个数n,把(PrintArg(args), 0)复制n份,也就会调用n次PrintArg(T t)
	//n次调用在PrintArg中打印,并且(PrintArg(args), 0)是一个逗号表达式,我们存储的值是后面的0,所以这里的a[]并不能直接访问到每一个参数
	int a[] = { (PrintArg(args), 0)... };
	cout << endl;
}

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2.4);
	ShowList(1, 2.4, string("helo")) ;

	return 0;
}

我们还可以把上面的语法精简一下,不用逗号表达式

template<class T>
int PrintArg(T t)
{
	cout << t << ' ';
	return 0;
}
template<class ...Args>
void ShowList(Args ... args)
{
	//此时就是把PrintArg的返回值当作了传参	
	int a[] = { PrintArg(args)... };
	cout << endl;
}

实例

上面讲解语法,每次都只取出一个参数,这种方式很难使用,我们下面写一个实例

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1) 
		:_year(year), _month(month), _day(day)
	{}

private:
	int _year, _month, _day;
};

template<class ... Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...);//构造函数就能按顺序拿到args的每一个分量
	return ret;
}
int main()
{
	//任意参数个数都可以初始化Date类型对象
	Date* p1 = Create();
	Date* p2 = Create(2023, 10);
	Date* p3 = Create(2023, 10, 30);

	Date d(2023, 10, 1);
	Date* p4 = Create(d);//这种方式也能够初始化

	return 0;
}

STL emplace系列

在这里插入图片描述
在这里插入图片描述
emplace系列就用了模板的可变参数Args,并且用了万能引用(左值 -> 引用折叠,右值 -> 右值引用)

list<pair<int, char>> mylist;
//empalce_back就能直接传pair的参数,而不需要用make_pair()或者pair()
//empalce_back会在CreateNode的时候,把参数new Node(args...),就去调用pair的构造函数了
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30,  'c'));//这种就是在new Node(value)的时候去调用pair的拷贝构造

mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });

这里从性能上,emplace_back和push_back并没有什么区别,因为这里pair的first和second都是内置类型,我们如果插入的是string map这种对于构造有有较大性能消耗的容器来说,即如下情况

list<pair<vector<int>, string>> mylist;
//empalce_back就能直接传pair的参数,而不需要用make_pair()或者pair()
//empalce_back会在CreateNode的时候,把参数new Node(args...),就去调用pair的构造函数了
mylist.emplace_back(vector<int>(), "string");//开了一个空对象的vector
mylist.emplace_back(vector<int>(5), "add");//vector<int>(5)创建具有5个元素的vector对象
mylist.emplace_back(make_pair(vector<int>(10, 0),  "st"));//10个元素并初始化为0的vector对象

mylist.push_back(make_pair(vector<int>(), "st"));
mylist.push_back({ {1, 2, 3, 4}, "str" });//initializer_list的调用方式

对于emplace_back,我们可以直接把pair的每一个分量一直传下去(是以引用的方式向下传递),直接去构造vector< int >和string,而对于push_back,pair是一个整体,必须先构造pair,然后再拷贝构造 / 移动构造(C++11) vector< int >和string
只能说emplace_back在这种情况下,略优于push_back,因为有移动构造的存在,拷贝构造 + 移动构造的效率只会比构造慢一点。

通过我对100w组数据进行插入测试,得到的时间花费如下,确实emplace_back在对于这种嵌套的插入情况下,用Args… args的语法会更为优秀
在这里插入图片描述

emplace的的函数声明中用到了万能引用,那么必定在实现中向下传递args的时候,会涉及到完美转发forward< Args >(args)…,其他的逻辑基本都是和push_back相同的

在这里插入图片描述
该截图就是VS2022中,empace的第二层,里面就包含了forward< Args > args

lambda表达式

可调用对象

在C、C++体系中,可调用对象包含了三类:

1、函数指针
语法很麻烦,用起来不方便

int f(int x, int y)
{
    return x + y;
}
int main()
{
    int (*fun_ptr)(int, int) = f;//fun_ptr就是函数指针
    fun_ptr(1, 2);//用函数指针调用f函数
}

2、仿函数
仿函数就是一个类,一个重载了operator()的类

class Less
{
	bool operator(const int e1, const int e2)
	{
		return e1 < e2;
	}
}
int main()
{
	vector<int> v = { 1, 5, 2, 3 };
	sort(v.begin(), v.end(), Less());//sort的传参的是变量、对象,不是类型,所以是Less()这个匿名对象
	return 0;
}

仿函数针对多次比较,比较逻辑相同的场景会很舒服,但是对于临时比较一次,或者多次比较,比较逻辑不同的情况就会显得很笨拙

3、lambda表达式
lambda表达式就是用来解决仿函数的问题的

引入

我们这里对商品进行排序

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

排序要求有:1、按照商品名字典序 2、价格升序 2、评价降序
我们注意到我们商品是自定义类型有多个属性,若想排序,函数指针?语法很复杂,不想用;仿函数?试试

struct CmpsNamedict
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._name > g2._name;//string重载了 < 
	}
};
struct CmpPriceLess//Less < 升序
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._price < g2._price;
	}
};
struct CmpEvaGreater
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._evaluate > g2._evaluate;
	}
};
int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,  3 }, { "菠萝", 1.5, 4 } };
    sort(v.begin(), v.end(), CmpsNamedict());
    sort(v.begin(), v.end(), CmpPriceLess());
    sort(v.begin(), v.end(), CmpEvaGreater());
    return 0;
}

那如果我又想价格降序,评价升序,或者又多了几个排序属性,并且仿函数一般是一个类对应一个比较,所以会显得非常臃肿

lambda表达式的使用

此时C++11中就出现了lambda表达式
lambda是局部匿名函数对象,我们来看看lambda的组成:
[capture-list] (parameters) mutable -> return-type { statement }

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

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

{statement}:函数体。在该函数体内,除了可以使用参数列表中的参数外,还可以使用所有捕获到的变量。

我们这里先简单针对例子使用使用

struct Goods
{
    string _name;  // 名字
    double _price; // 价格
    int _evaluate; // 评价
    Goods(const char* str, double price, int evaluate)
        :_name(str)
        , _price(price)
        , _evaluate(evaluate)
    {}
};
int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,  3 }, { "菠萝", 1.5, 4 } };
	// lambda是函数匿名对象,我们可以用auto自动判断类型
	auto CmpPriceLess = [](const Goods& g1, const Goods& g2)->bool{
		return g1._price < g2._price;
	};
	sort(v.begin(), v.end(), CmpPriceLess);
	
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool{
		return g1._name < g2._name;//字典升序
      });
    sort(v.begin(), v.end(), [](const Goods& g1, cosnt Goods& g2){//如果返回值明确,可以省略返回值
		return g1._price > g2._price;
	};
    return 0;
}

除了作为匿名函数对象,作为sort的参数之外,我们来看看其他场景

简单的交换

int main()
{
	string x = "XHE", y = "DA";
	//交换x和y
	auto swap = [](string& x, string& y){
		string tmp = x;
		x = y;
		y = tmp;
	};
	cout << x << " " << y << endl;
	swap(x ,y);
	cout << x << " " << y << endl;
	return 0;
}

下面是匿名使用lambda函数

int main()
{
	string x("dafs"), y("asdf");
	//匿名调用一次函数
	[](string& _x, string& _y){//这里的_x _y是形参
		string tmp = _x;
		_x = _y;
		_y = tmp;
	}(x, y);//这里的x和y是实参
	cout << x << " " << y << endl;	
	return 0;
}

然后是关于捕捉列表的问题
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
[var] - 值传递var
[=] - 值传递捕捉所有父作用域中的变量,包括this指针
[&var] - 引用捕捉,从捕捉列表的角度,建立lambda内外的连接
[&] - 引用传递捕捉所有父作用域中的变量,包括this指针
[this] - 值传递的方式捕捉this指针

对于值传递的捕捉,内部使用的拷贝的捕捉变量,并且是只读的,也就是const的,内部无法对捕捉的值进行修改,如果真需要值传递,又想对值进行修改,那么就需要使用mutable,需要常量性;
对于引用捕捉,和函数引用传参的性质一样,内外是同一个变量,并且可以进行修改,会影响到lambda函数外
不能重复捕捉,比如:[=, a] [=]已经表示所有的值捕捉了,但是我们又捕捉了a,就重复捕捉了

void Func(int x, int y)	
{
	cout << "Func(int x, int y)" << endl;
}
class AAA
{
public:
    bool operator()(const string& x)
    {
        cout << "bool operator()" << endl;
        return true;
    }
};
string env;
int main()
{
	string x = "asdf", y = "asdf";
	[](string& _x, string& _y){
		string str = _x + _y;
		cout << str << endl;//cout<< 实质还是调用函数,iostream里面的函数,全局函数
		Func(1, 2);//普通的全局函数调用
		
		AAA a;//仿函数类对象
		a(_x);//调用仿函数
	
		x;//err,x是局部变量
		cout << env << endl;//env是全局变量
	};
	[swap, x, y](vector<int> v1){
		for(auto e : v1) { cout << e << " ";}
		cout << x << endl;//使用局部变量x 打印"XHE"
		
		string r1, r2;
		swap(r1, r2);//使用函数对象swap
	
		swap(x, y);//err,需要加mutable
	};

	
	int a, int b;
	const c;
	auto e = [swap, &x, &y, &c, a, b]()mutable{
		swap(x, y);
		a = b = 0;//right, 有mutable 但是不会真正影响到a和b的值
		c = 10;//err,外面是const,那么就是const引用捕捉
	};
	e();//调用e lambda函数
	return 0;
}

lambda函数的原理

f1和f2可以互相赋值吗?不行

auto f1 = [](int x, int y){return x + y;};
auto f2 = [](int x, int y){return x + y;};
f1 = f2;//err

也就是说f1和f2的类型是不同的,我们通过typeid来查看f1和f2的类型

auto f1 = [](int x, int y){return x + y;};
auto f2 = [](int x, int y){return x + y;};
cout << typeid(f1).name();
cout << typeid(f2).name();

在这里插入图片描述
在这里插入图片描述
发现这两者类型确实不一样,有些编译器上是一串16进制的字符串,这个字符串是UUID,有些编译器上的typeid显示的是< lambda_number >,实质上就是UUID

UUID是通用唯一识别码:大佬搞出来的一个算法,这个算法生成的UUID能够保证在全球几乎找不出来两个相同的UUID,使得能够保证唯一性

也就是说lambda的类名就是这个一些前缀class ~~~ 加上UUID,这就能保证lambda函数之间不会生成一样的类名

lambda和范围for的实现逻辑很类似,都是套了一层已有的东西,范围for套了一层迭代器,lambda也就是套了一层仿函数,也就是说,编译器通过我们这里的lambda语法,自动生成了一个仿函数,我们auto f1 ,这个f1就是函数对象,f1(1, 2)这里其实 实质上就是调用了operator()

我们可以通过反汇编观察到:
在这里插入图片描述

function

可调用对象的类型有三种,函数指针、仿函数、lambda表达式,这三种类型都可以在C++中使用
如果我们将可调用对象类型作为模板参数时,我们就不知道这个模板参数到底是函数指针还是仿函数或者是lambda表达式,比如下面这种情况

double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

template<class F, class T>
T Func(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
int main()
{
	cout << Func(f, 1.1) << endl;// 函数名就是函数指针
	
	cout << Func(Functor(), 1.1) << endl;// 仿函数对象
	
	cout << Func([](double d)->double { return d / 4; }, 11.11) << endl;// lambda表达式
	return 0;
}

并且这样经过编译,会实例化生成三份函数代码,我们想节约空间,只想实例化为一份呢?或者我们想把三种可调用对象存储到容器中,容器类型怎么写?那是否还需要创建三个不同的容器?

int Func(int a, double b)
{
	cout << "int Func(int a, double b)" << endl;
	return 0;
}
class Functor
{
public:
	void operator()()
	{
		cout << "void operator()()" << endl;
	}
};
int main()
{
	int (*fptr)(int, double) = Func;//fptr是函数指针  int(*)(int, int)是类型
	fptr(1, 2.1);

	Functor f;//Functor是仿函数类型
	f();
	
	auto lambda = [](int a, int b) -> int { return a + b; };
	lambda_type = decltype(lambda);
	// 但是对于lambda表达式来说,类型是一串随机生成的UUID,除了decltype,我们无法获得其类型
	return 0;
}

当然会有人说,不管是函数指针,仿函数,lambda表达式,都能够用decltype获取对象类型,但是decltype是通过对象,拿到对象的类型,但始终都是三种类型,始终是不能放到vector等容器里面的

因此C++11推出了一种包装器function,是一种针对函数的包装器,在头文件functional中,是一个类,Ret是被调用函数的返回类型,Args是被调用函数的形参类型,将参数类型和返回值类型都相同的各种可调用对象统一起来,其语法上比较特殊,最好记一记
在这里插入图片描述

#include <iostream>
#include <functional>
using namespace std;
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

template<class F, class T>
T Func(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}

int main()
{
	function<double(double)> f1 = f;
	//       返回类型 形参类型
	function<double(double)> f2 = Functor();
	function<double(double)> f3 = [](double d){ return d / 4; };
	return 0;
}

也就是说,包装器类型,能够存储函数指针、仿函数、lambda函数,比如上例中function<double(double)>类型的包装器,能够存储这三种可调用对象,此时我们就能够把这三种可调用对象封装到一个vector数据容器中了

所以包装器的本质是在解决可调用对象的类型问题

int main()
{
	function<double(double)> f1 = f;
	//       返回类型 形参类型
	function<double(double)> f2 = Functor();
	function<double(double)> f3 = [](double d){ return d / 4; };

	//vector<function<double(double)>> vf = { f1, f2, f3 };//使用已经用function定义的对象
	vector<function<double(double)>> vf = { f, Functor(), [](double d){ return d / 4; };//直接传递可调用对象
	
	double d = 10.5;
	for(auto f : vf)
	{
		cout << f(d++) << endl;//此时就能够存储使用这三种可调用对象
	}
	for(int i = 0; i < vf.size(); i++)
	{
		cout << vf[i](d++) << endl;
	}
	return 0;
}

包装器是一种命令对应动作的逻辑,比如Linux下cd / 命令对应的动作就是跳转根目录

下例就可以简单体现

例题

在这里插入图片描述

// 此题会涉及到四种运算,+ - * / 我们想要字符串"+"和int add(int, int)的连接,那么就需要一个map,first存储string,而second就有考究,我们可以直接使用int (*cal)(int ,int)这种函数指针,也可以用CAL cal仿函数,也可以用[](int x, int y)->int{return x + y;}lambda表达式,但是我们为了支持各种写法,这里使用函数包装器
int evalRPN(vector<string>& takens)
{
	// + 命令对应x + y动作
	// - 命令对应x - y动作...
    map<string, function<int(int, int)>> FuncMap = {
        {"+", [](int x, int y)->int {return x + y; }},
        {"-", [](int x, int y)->int {return x - y; }},
        {"*", [](int x, int y)->int {return x * y; }},
        {"/", [](int x, int y)->int {return x / y; }}
    };//精华
    stack<int> st;
    int i = 0;
    while (i < takens.size())
    {
        string str = takens[i];
        if (FuncMap.count(str))//如果str在FuncMap中有对应的key,说明str是操作符,进行运算
        {
            int right = st.top();
            st.pop();
            int left = st.top();
            st.pop();
            int tmpret = FuncMap[str](left, right);//此时就能够通过我们的map对应,直接获取到str所对应的运算,这样就不用再四个if str == "+"的时候....大大浓缩了代码,使代码更加简洁
            st.push(tmpret);
        }
        else st.push(stoi(str));
        i++;
    }
    return st.top();//此时st中只有一个数,就是结果
}

上面这个例子是“动作”的参数和返回值都相同的情况,我们还可以把各种参数和返回值的包装器封装在一个类中,我们通过调用类中的我们设定的参数和返回值对应的包装器对象,就能够使用到不同参数和返回值对应的"动作"

下面是我自己随意的封装的一个类

#include <iostream>
#include <string>
#include <map>
#include <functional>
using namespace std;

class AllFunction
{
public:
    //Put操作是加入动作
    void Put1(pair<string, function<int(int, int)>> kv)//处理参数是两个int 返回值是int的动作
    {
        _FuncMap1[kv.first] = kv.second;
    }
    void Put2(pair<string, function<double(double, double)>> kv)//处理参数是两个double,返回值是double的动作
    {
        _FuncMap2[kv.first] = kv.second;
    }
    void Put3(pair<string, function<string(const string&)>> kv)//处理参数是const string&,返回值是string的动作
    {
        _FuncMap3[kv.first] = kv.second;
    }
    void Put4(pair<string, function<void(string)>> kv)//处理参数是string,无返回值的动作
    {
        _FuncMap4[kv.first] = kv.second;
    }

    //get是通过指令访问动作
    map<string, function<int(int, int)>>& getMap1()
    {
        return _FuncMap1;
    }
    map<string, function<double(double, double)>>& getMap2()
    {
        return _FuncMap2;
    }
    map<string, function<string(const string&)>>& getMap3()
    {
        return _FuncMap3;
    }
    map<string, function<void(string)>>& getMap4()
    {
        return _FuncMap4;
    }
private:
    map<string, function<int(int, int)>> _FuncMap1;//动作一大类
    map<string, function<double(double, double)>> _FuncMap2;//动作二大类
    map<string, function<string(const string&)>> _FuncMap3;//动作三大类
    map<string, function<void(string)>> _FuncMap4;//动作三大类
};
class Functor
{
public:
    string operator()(const string& str)
    {
        cout << "const string& operator()(const string& str)" << endl;
        return str;
    }
};
void F(string str)
{
    cout << "void F(string str): " << str << endl;
}
int main()
{
    AllFunction af;
    //插入各种类型的动作,并且我们能够插入以函数指针、仿函数、lambda表达式实现的动作,十分兼容
    af.Put1(make_pair("pwd", [](int x, int y) { return x + y; }));
    af.Put2(make_pair("cd", [](double x, double y){ return x + y; } ));
    af.Put3(make_pair("vim", [](const string& str)->string{ return str; } ));
    af.Put3(make_pair("rm", Functor()));
    af.Put4(make_pair("void", F));
    void (*str)(string) = F;//定义了void作为返回值string作为参数的函数指针,,这个指针指向了F函数
    af.Put4(make_pair("test", str));

    int ret1 = af.getMap1()["pwd"](1, 2);//做动作
    double ret2 = af.getMap2()["cd"](1.2, 2.3);//做动作
    string ret3 = af.getMap3()["vim"]("hello");//做动作
    string ret33 = af.getMap3()["rm"]("hello");//做动作

    af.getMap4()["void"]("str");
    af.getMap4()["test"]("test");

    cout << ret1 << endl;
    cout << ret2 << endl;
    cout << ret3 << endl;
    cout << ret33 << endl;
    
    return 0;
}

除了上面这种命令对应动作的用法,还会在网络编程中起到很大作用,大家可以自行了解

bind

bind - 英文意:绑定
是绑定函数包装器,需要搭配placeholders命名空间使用,placeholder中有_1… _20变量,这些变量代表了函数的第一个参数,第二个参数,…

调整参数位置

我们可以通过bind绑定,调整参数调用时的前后位置,这一般用于一些接口的顺序非常的不合理的时候使用
在这里插入图片描述
class Fn可以是所有可调用对象,Args是重调整顺序的参数列表,返回值是调整后的新的可调用对象,可以用Function包装器接收

int Sub(int a, double b)
{
	return a - b;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	function<int(double, int)> rSub1 = bind(Sub, placeholders::_2, placeholders::_1);
	function<int(int, double)> rSub2 = bind(Sub, placeholders::_1, placeholders::_2);
    function<double(double)> rDiv = bind(Functor(), placeholders::_1);
	auto lambda = [](double x, double y)->double { return x * y;};
	function<double(double, double)> rMul = bind(lambda, placeholders::_2, placeholders::_1);

	//甚至可以把调整过顺序的可调用对象,再次让bind进行处理
	function<double(double, double)> rMul1 = bind(rMul, placeholders::_1, placeholders::_2);

	cout << rSub1(10, 5) << endl;//-5
	cout << rSub2(10, 5) << endl;//-5
	cout << rDiv(10) << endl;//3
	cout << rMul(2, 3) << endl;//6,*体现不出来,但语法是这样的
	return 0;
}

减少参数

除了调整位置,还能够减少参数的传参个数,这一般是用于处理一些系统接口、库函数接口、再或者同事写的垃圾接口时,一些参数的值是长期固定的,用起来很麻烦的情况,通过例子讲解减少参数的细节

讲到减少参数的bind使用,我们需要再次明确placeholders后面的_1 _2 _3的含义:_1表示的是Fn函数中第一次出现的参数,_2是第二次数显的参数,我这里的参数需要是变化的量,我们通过bind传值固定下来的值,不参与placeholder的排序,所以我们bind返回的可调用对象的传参个数,就是在调用bind时placeholder的使用个数
在这里插入图片描述

double Plus(int a, int b, double rate)
{
    return (a + b) * rate;
}
double PPlus(double rate, int a, int b)
{
    return (a + b) * rate;
}
double PPPlus(int a, double rate, int b)
{
    return (a + b) * rate;
}
int main()
{
    function<double(int, int)> ret1 = bind(Plus, placeholders::_1, placeholders::_2, 3.5);//我们不能placeholders::_3 = 3.5,编译错误
	function<double(int, int)> ret2 = bind(PPlus, 3.2, placeholders::_1, placeholders::_2);//注意,placeholders::永远是从_1 _2 一直按顺序使用下去
	//理解上,可以认为,固定的值就不参与参数的调整,所以就不在placeholders的排序中
	function<double(int, int)> ret2 = bind(PPlus, placeholders::_1, 3.2, placeholders::_2);
    return 0;
}

会有人说,可以用缺省参数替代,但是这样不好,缺省参数的默认值,是"静态"确定的,也就是我们定义时直接确定的,而通过bind固定参数的方式是可以通过我们的一系列计算,把这个计算结果传给bind,这是一种"动态"确定,这样更加灵活

类中成员函数的绑定

我们对于类中的成员函数的绑定问题:
1、需要对绑定的成员函数指定类域,指定类型前最好加一个&(静态的成员函数可以不加,非静态的成员函数必须加)
2、对于静态的成员函数,我们的placeholders和全局函数的处理是一样的,但是对于非静态的成员函数,由于有this指针的存在,所以需要特殊处理,给bind的args可变参数部分的第一个参数给上匿名对象 或者 创建一个类对象,传对象的地址,这两种方式都可以

class test_class
{
public:
    static int add(int x, int y)
    {
        return x + y;
    }
    double sub(int x, int y, double rate)
    {
        return x - y;
    }
};
int main()
{
    function<int(int, int)> radd = bind(&test_class::add, placeholders::_1, placeholders::_2);
    test_class tc;
    function<double(int, int)> rsub1 = bind(&test_class::sub, &tc, placeholders::_1, placeholders::_2, 2.4);
    function<double(int, int)> rsub2 = bind(&test_class::sub, test_class(), placeholders::_1, placeholders::_2, 2.4);
    cout << rsub1(1, 2) << endl;
    return 0;
}

绑定bind的底层和lambda类似,也是生成了一个仿函数,仿函数中通过我们传的 类对象 或者 对象的指针,分别通过 . 或者 -> 来访问类,我们的传递的第一个参数test_class::sub就能够确定是访问类中的哪个函数,然后后面的这些placeholder就用于控制rsub2到这个仿函数的传参顺序,也就是说,我们并没有通过bind真正的改变函数的参数顺序,只不过是套了一层外壳,外壳的顺序是我们想要的顺序,内部调用的顺序就是函数定义的顺序。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失去梦想的小草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值