C++11新特性

目录

1 统一的列表初始化

2 简化的声明方式

3 范围for

4 智能指针

5 STL的一些变化

6 右值引用

7 新的类功能

8 lambda表达式

9 可变参数模板

10 包装器


1 统一的列表初始化

在C++早些版本或者直接说在C语言中,我们就经常使用 { } 来初始化一个数组或者一个结构体或者结构体的一部分,这是我们C语言就学习过的语法,对结构体进行部分初始化的时候需要按照内部成员的声明顺序来,当然在C语言中,使用{ }中间初始化时必须加赋值符号

 struct A
{
	int a;
	char b;
	const char* str;
};	

    int arr[] = {1,2,3,4,5};
	A a1={1};
	A a2={ 1,'c' };
	A a3={1,'c',"abcdefg"};

而在C++11中,则扩大了这个 { } 的使用范围,首先第一个升级就是,所有的类型,不管是自定义类型还是内置类型,都可以使用花括号进行初始化,当然对于自定义类型而言需要支持对应的构造函数,因为对于class的自定义类型是相当于去调用构造函数进行初始化。

class B
{
public:
	B(int b,char ch,const char*str)
		:_b(b),_ch(ch),_str(str)
	{}

private:
	int _b ;
	char _ch;
	const char* _str;
};	
   

    int x = { 10 };
	char c = { 'o' };
	A  a= {1,'c'};
	B b = {1,'c',"ashfjk"};

那么我们的自定义类型的 new 出来的对象就不仅可以使用 () 来调用构造了,也可以使用{ } 来调用构造函数,以及new一个数组的时候也可以用一个花括号来初始化。

	int* parr = new int[5]{ 1,2,3 };
	int* parr1 = new int[5]{1,2,3,4,5};
	A* pa = new A{ 1,'c',"aaa" };
	B* pb = new B{ 1,'c',"aaa" };
	B* pb1 = new B(1,'c',"aaa");

	B* pbarr = new B[3]{ {1,'a',"aaaa"} , {2,'b',"bbbb"} , {3,'c',"cccc"} };

第二点升级就是,可以把赋值符号省略

	int x { 10 };
	char c  { 'o' };
	A  a {1,'c'};
	B b {1,'c',"ashfjk"};

当然,这样用起来很怪就是了,但是我们也要知道有这种语法,可以这样用。

关于初始化,C++11还新增了一个类来支持自定义类型的初始化

std::initializer_list

比如我们的 stl 容器可以这样初始化,直接在定义的时候就给上一堆初始值

	vector<int> v1{1,2,3,4};
	list<int> lt1{1,2,3,4};
	set<int> s1{1,2,4,5,6};

当然这里也可以加上一个赋值符号,所有的花括号进行初始化在C++11之后都可以把赋值符号省略,这是怎么做到的呢?

其实是因为 C++11 把这种花括号的列表转换成了一个类对象,是什么类型呢?我们可以看一下

就是一个初始化列表,这个初始化列表底层我们可以理解为就是一个常量数组,就跟我们的常量字符串一样,那么为什么能够直接初始化 vector 等容器呢?

很简单,提供一个相关的构造函数就行了,拿vector来举例

C++11之后,提供了一个 initializer_init 版本的构造函数,支持用一个初始化列表来进行初始化。我们也可以把 initializer_list理解为一个容器,是容器就有迭代器,而他的迭代器就是一个原生指针,begin 指向它的开始,end 指向它的结束的下一个位置。

但是这里和上面所讲的花括号直接进行初始化不一样,上面的花括号是在直接调用构造函数,而这里的初始化列表则是把这个花括号识别成一个initializer_list 或者简单来说就是一个常量数组,然后调用了 initialzer_list 版本的构造函数进行构造。

那么我们自己写的容器能够使用这个初始化列表来构造吗? 当然可以,前提是你必须自己去重载一个用初始化列表进行构造的构造函数。这个构造函数也很好些,用一个范围 for 遍历初始化列表的同时不断尾插就行了。

除了内置类型,自定义类型也能这么玩,比如:

	vector<B> v1 = { {1,'a',"aaa"},{2,'b',"bbb"} ,{3,'c',"ccc"}};

但是我们要注意这两层花括号的含义是不同的,内部的花括号是在用花括号里面的参数进行构造匿名对象,而外部的花括号则是将这些匿名对象存在了一个初始化列表或者说一个常量数组里面,然后调用vector的初始化列表的构造函数进行构造。

其他的容器也能这样进行初始化,只要他的构造函数支持了用初始化列表进行构造。

2 简化的声明方式

auto 关键字

首先就是我们之前就一直在使用的  auto ,使用 auto 来定义变量的时候编译器能够根据他的初始化的值来自动推导变量的类型,这样一来对于那些名字很长的类型我们就方便很多了,比如一些迭代器。

decltype 关键字

decltype关键字能够将变量的类型声明为表达式指定的类型

怎么理解呢? 就是你给decltype关键字传一个变量,然后这个关键字就能够根据你穿的这个变量将它的类型提取出来,我们就可以使用这个提取出来的类型来定义变量。

	int a = 10;
	float b = 1.4f;
	double c = 1.4;

	decltype(a * b) d = a * b;
	decltype(a * c) e = a * c;
	decltype(b * c) f = b * c;

这用在一些我们不确定表达式返回值类型的场景下。

当然有的时候 auto 也能行,但是 auto 有一个最大的局限,就是必须给初始值才能推导,而我们的decltype则只需要给出我们未来要接受的表达式就能够进行定义,不需要给初始值。

	decltype(a * b) d; // auto 只能这么用 auto d = a * b; 
	d = a * b; 
	decltype(a * c) e ;
	e == a * c;
	decltype(b * c) f;  
	f = b * c;

不过说实话,在大多数场景下我们的 auto 就足够了,因为我们没必要提前定义出来,可以在接收返回值的时候再使用auto来定义。

讲到 decltype ,这里就不得不提起另一个 C++11 的东西,就是 typeid ,typeid我们上面也用了,它的作用是返回  变量的类型的字符串  ,而我们的decltype是提取类型 ,要注意分清楚他们的区别。

最后就是以前讲过的 nullptr ,我们说过,C++中 NULL 的值直接就定义成了 0 ,字面量,那么他是有歧义的,很多时候都是把 NULL 识别成 int 。 那么为了弥补这个漏洞,C++11 就搞了一个新的关键字 nullptr 来表示空指针,也就是 (void*) 0 。

3 范围for

范围for就是编译器将其替换成了迭代器的方式进行的遍历,我们以前在实现stl容器的时候就已经讲解过了。

4 智能指针

智能指针由于很重要同时内容很多,跟其他的新特性也有关系,所以我们在后面会单独出一篇文章来讲解。

5 STL的一些变化

C++11对我们的STL容器也进行了更新。

首先就是推出了新的容器:array,forward_list,unordered_set,unordered_map

array就是C++的静态数组,需要指明数据个数。他的C语言的静态数组的区别就是他的方括号是运算符重载来实现的,所以他的越界的检查更加严格,除此之外就没什么特别的了。

forward_list 就是单链表,无头单向不循环链表,其实单链表的更新真的没什么用,STL库里的单链表也是不提供尾插尾删的。最难受的是,他的 insert 和erase 还和我们常用的不一样,他是insert_after 和 erase_after ,在指定位置之后进行插入,就很奇怪。就算我们自己实现一个单链表出来也不是很难,讲真没有什么用。

最后就是unordered系列容器,哈希set和哈希map还是很有用的,我们在前面也已经模拟实现过了,那么这里也就不多赘述。

第二个更新就是容器的新方法

我们在官方库的文档中就能看到有很多C++11的接口,就比如我们上面讲了的初始化列表的构造函数,其他的比如还提供了 cbegin cend 等专门给const对象来调用迭代器的接口。

当然最重要的接口都是跟C++11的新特性有关,比如我们的 移动构造和移动赋值,以及emplace系列的接口

这些接口我们后续讲到右值引用和可变模板参数的时候会细讲。

6 右值引用

这是C++11中很重要的一点。

首先,什么是右值呢? 有右值,那肯定还有于之相对的左值,左值又是什么呢?

难道就是简单的在赋值符号左边的就是左值,右边的就是右值吗?那么下面的a是左值还是右值呢?

	int a = 10;
	int b = a;

其实,没怎么简单,我们赋值符号右边的也有可能是左值。

左值就是一个表示数据的表达式,如变量名或者解引用的指针,我们可以获取他的地址或可以对他进行赋值,左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。

左值都是可以取地址的,这是左值的最基本的条件。

同时有的左值并不一定能够进行赋值,比如const的左值,初始化之后就不能再进行赋值了。

常见的左值如下:

	int a = 10;  //a是左值
	int b = a;  //b是左值
	
	int* p = &a;  //p是左值

	*p = 10;  //*p是左值

那么左值引用就是对左值的引用,也就是给左值取别名

	//左值引用
	int& ra = a;
	int& rb = b;
	int*& rp = p;
	int& rpa = *p;

那么什么是右值呢?

右值也是一个表示数据的表达式,比如字面常量,表达式的结果,函数返回值(不是左值引用返回的时候),匿名对象等。 右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,最后,右值不能取地址

	int add(int x, int y)
	{	
		return x + y;
	}

	//常见的右值
	int a = 10; //10是右值
	int b = 10 + 20; //10+20的返回值是右值
	int c = a + b; //a+b的返回值是右值

	A(2,'a',"aaa");  //匿名对象是右值

	int sum = add(a,b); //add(a,b)的返回值是右值

右值引用就是对右值的引用。

我们可以发现,这些所谓的右值好像都是一些字面量或者是一些临时的变量,马上就会销毁的那种

右值引用的语法就是 && ,也就是比左值引用再多一个 & 就行了

	int&& r1 = 10;
	int&& r2 = 10 + 20;
	int&& r3 = add(10, 20);
	B&& rb = B(10,'a',"aaa");

但是我们似乎还记得,左值引用我们曾经用来引用过函数的传值返回的返回值啊,我们使用的是const的左值引用,这又是为什么呢?

左值引用只能引用左值,不能引用右值

但是const左值引用既可以引用左值,也可以引用右值

	const int& r1 = 10;
	const int& r2 = 10 + 10;
	const int& r3 = add(10,10);

为什么const左值引用能够引用右值呢?

我们发现,右值不能取地址,而且右值要不就是具有常性,要不就是即将销毁,我们都无法对其进行修改,那么我们使用const左值引用就没问题了,因为权限是可以平移的。

但是有一个问题,比如上面的r1和r2,他们引用的值要不就是字面量,这种值是存在常量区的,而r2引用的则是一个表达式的返回结果。

不管怎么说,r1和r2就是一个左值了,或者说具有左值属性,因为他们确实是在赋值符号的左边的,那么他们所引用的对象存在哪呢?不是都已经销毁了吗?

我们可以将r1和r2的地址打印出来

我们能看出来,r1和r2的地址都是在栈区的,那就证明了他们所引用的对象的地址是在栈区,所以这些右值变量被const左值引用之后,虽然他们本来是马上要销毁的,但是在栈区又拷贝了一份,存在当前栈帧中

同时,对于函数的这个返回值,我们也特意在函数返回之后,再定义了一个变量,我们发现新定义的变量是在 r3 的后面,说明这个函数的返回值也拷贝了一份放在了我们的当前函数栈帧中。

右值引用只能引用右值,不能直接引用左值。

但是右值引用可以引用move之后的左值

move是什么我们后面会讲到。

但是这我们就很纳闷了,明明const左值引用可以引用右值,那么为什么C++11要增加右值引用的概念呢?右值引用和const左值引用差别在哪里呢?

我们先来见一个最明显的差别:

这时候我们就能发现,他们虽然拷贝能对右值进行引用,对于这种字面常量都是拷贝一份下来引用,但是const左值引用是不能修改的,而右值引用拷贝下来的那个数据是能修改的。

但是这个好像并没有很大的意义,不就是拷贝了一份数据吗,而且我们本来就没有对常量进行修改的必要,倒不如直接存一个变量,不也是拷贝了一份下来吗?

那么右值引用到底有什么意义呢?

首先我们要思考一下,引用是用来干什么的?引用有什么意义呢?

引用最大的意义就是减少函数传参和传返回值的拷贝

当然这里的引用目前来说是相对于左值引用而言的,因为我们还没有真正了解右值引用。

但是,我们的左值引用彻底解决了这里的问题吗?传参的问题是彻底解决了,因为无论是传左值还是传右值我们都能够使用左值引用或者const左值引用来接收,但是传返回值呢?如果返回值返回的还是传参传过来的那个左值引用,那么没什么问题,但是返回值我们如果是一个函数内的局部对象呢?函数返回函数的栈帧就销毁了,他的局部对象也就跟着销毁了,那么我们是用左值引用来返回就不行了,因为引用就是给这个变量取别名,函数返回之后这个变量都被销毁了,那么这个引用不就是类似于野指针了吗?

那么如果我们函数返回的是临时对象或者局部对象,我们就不能使用左值引用返回,只能传值返回。

但是传值返回又有一个最大的问题,就是拷贝,如果返回的是一个需要深拷贝的复杂的自定义类型对象,比如 vector<vector<string>> 这样的对象,那么拷贝的代价是非常大的。比如我们自己写一个包含提示信息的简单的string。

	class string
	{
	public:

		string() { cout << "构造" << endl; }

		string(const char* str)
		{
			_capacity = strlen(str);
			_size = strlen(str);
			reserve(_capacity + 1);
			memcpy(_str,str,strlen(str)+1);

			cout << "构造" << endl;
		}

		string(const string& s)
		{
			_capacity = s._capacity;
			_size = s._size;
			reserve(_capacity+1);
			memcpy(_str,s._str,_size);
			cout << "深拷贝" << endl;
		}

		~string()
		{
			delete[] _str;
			_size = _capacity = 0;
			cout<<"析构"<<endl;
		}

		string& operator=(const string& s)
		{
			_str = new char[s._capacity+1];
			_size = s._size;
			_capacity = s._capacity;
			memcpy(_str,s._str,_capacity);
			cout << "拷贝赋值" << endl;
			return *this;
		}

		void swap(string s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity,s._capacity);

		}
		void reserve(size_t n)
		{
			char* newstr = new char[n];
			memset(newstr, 0, n);
			delete[] _str;
			_str = newstr;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};

简单的比如我们就需要传值返回一个局部的string对象。

mystring::string func()
{
	mystring::string ret;
	ret = "aaaaa";
	return ret;
}

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

如果我们使用对象来接收的话,从 return 到 接收到返回值中间会有两次拷贝,一次是通过返回的对象来拷贝构造一个临时对象,第二次是通过临时对象拷贝构造我们的ret,当然新的编译器都会将这里转化为一次拷贝。直接用 func 中的返回对象来构造我们的接收对象。

在编译期没有优化的时候,当我们的返回值很小的时候,能放在寄存器的话,就是通过寄存器将返回值带回来,然后拷贝给接收返回值的变量,这之间是两次拷贝。而如果返回值很大时,我们以前说过他会在合适的位置先将返回值拷贝下来,然后再释放函数栈帧,最后再将拷贝下来值再一次拷贝给接收返回值的变量。而这个中间变量其实是在两个函数栈帧之间的。我们以前C语言阶段就知道了在func函数栈帧创建之前,先要进行参数的压栈,也就是将实参拷贝一份给形参。而如果编译器识别到我们的返回值很大时,他也会在这里预留一段空间用于存储函数的返回值,然后再esp不断pop的时候将这里存的值拷贝给接收返回值的变量。

当然,在编译器看来这次拷贝也没必要,所以就优化为了一次直接拷贝。

尽管编译器进行了优化,但是还是会存在一次深拷贝,如果对象很大的话效率还是会受到影响。

那么在C++11之前要如何避免深拷贝呢? 

使用输出型参数。

我们可以传外部定义好的一个string对象,引用传参 或者 指针传参,然后在函数体中通过引用或指针直接对我们的外部的对象进行操作,这样就避免了拷贝。

当然也可以在函数体中使用new在堆区申请和创建对象,然后将起始地址传回去。但是这样做的话需要外部进行堆空间的释放,不太合适。

除了输出型参数,还能有其他办法减少拷贝吗?

这里返回值使用左值引用肯定是解决不了问题了,那么改成右值引用就能解决问题了吗?

注意,我们这里不能使用引用返回的根本原因是因为返回值是一个临时对象,当出了这个函数的作用域之后它就自动销毁了。我们的左值引用无法解决,const左值引用无法解决,同样的,我们的右值引用也是无法解决的。同时,我们的ret临时变量是一个左值,他无法直接使用右值引用。就算我们接收参数时使用右值引用,但是返回类型也只能是传值返回,这样一来右值引用的还是中间生成的临时对象,而这个临时对象马上就销毁了。

所以我们的解决问题的根源还是要让这个变量出了作用域不销毁才能使用引用接收返回值,但是我们却做不到让一个函数内的临时变量出了函数作用域不销毁。

那么我们可不可以换一种思路,既然这个对象出了作用域就要销毁,那么也就是说,在我们接收返回值或者接收返回值之后,这个返回的变量要被析构,我们无法在使用它内部的资源了。那么我们有没有一种办法,直接将这个即将销毁的对象的资源转移到我们的接收返回值的对象来呢。这样做的原因有两点:

1 返回变量即将销毁,这个资源如果不转移就要被销毁了。 2 转移资源的消耗比进行深拷贝的消耗要高得多。

那么我们要怎么才能支持这种资源的转移呢?这是后话。

我们先来尝试写一个新的成员函数,我们以前的拷贝构造写了一个左值引用版本的,现在有了右值引用,是不是也能够写一个右值版本的出来?

那么右值引用版本的构造要怎么写呢?

首先什么情况下需要有构造函数?自定义类型。 那么什么情况下会用到右值引用的拷贝?自定义类型右值好像我们想来想去也就是匿名对象这种临时对象。而如果他是类似于匿名对象这种,它的生命周期就只有这一行语句,那么我们还需要进行一次深拷贝吗?是不是就可以按照上面的说法,将这个即将被销毁的对象的资源转移到要构造的对象中来,反正这个匿名或者临时对象也马上就要析构了,好不容易构造他的时候申请下来资源,就这样随他而逝真的有点可惜了,不如传承给新的对象。

那么我们的右值引用的构造就只需要一个 swap ,交换两个对象之间的资源就行了,由于是构造,那么this的资源初始的时候就是空,换过去让他析构就行。

我们把这样的右值引用版本的构造叫做移动构造

		string(string&& s)
		{
			swap(s);
			cout << "移动构造" << endl;
		}

但是我们实际使用下来好像发现并不是如此;

	mystring::string s = mystring::string("aaaa");

我们会发现,在这种场景下编译器依旧会优化,将构造匿名对象之后在移动构造 s ,优化为直接使用"aaaa"构造我们的s对象。

那么在什么场景下能够用到这个函数呢?

我们前面讲右值的概念,右值包括函数返回值,匿名对象,表达式返回值,字面常量等,那么函数返回值也是右值我们再换回之前的案例来试一下。

当然这最终还是和编译器对C++11的支持有关系。

我们发现,由于中间的这次拷贝会被编译器优化掉,那么整个返回过程就变成了直接调用移动构造,将 func 中的 ret 的资源移动到了 main 中的 ret 中。

注意不要用vs2022来测试,因为最新的vs2022的优化十分过分,语法识别到我们在func函数中是直接构建一个对象然后返回给main的ret时,他就直接使用 "aaaaa"来构造main中的ret了,甚至都没有在func中真正构造出他的ret对象。

目前来看移动拷贝确实减少了深拷贝的次数。

但是这时候我们就有一个疑问了,为什么这里的函数返回值会去调用右值引用的版本?很简单,因为我们说了 函数的 传值返回的返回值就是经典的右值。 但是我们又会想起上面的概念,右值是不可被取地址的,左值是可以取地址的值,而我们的ret明明是可以取地址的,按理来说它具有左值的性质啊,为什么会被调用右值版本呢?

其实ret本身是一个左值,这是毫无疑问的,我们所说的函数返回值指的是中间的拷贝的临时对象,而不是return 的这个对象本身。 但是由于编译器将中间的拷贝给优化了,所以我们在最后这一条 return 语句的时候,也可以把这个左值看成右值。

其实我们区分左值和右值还有一种划分的方法,传统意义上的左值就是可以取地址的值,传统意义上的右值就是不能取地址的值以及一些临时对象和表达式的返回值这种马上就会被销毁的值。

在C++11引入了右值引用的概念之后,我们其实可以将表达式划分为 左值,将亡值,纯右值。

在C++11之前右值和纯右值是等价的,而在C++11之后,右值就分为了纯右值和将亡值。我们上面讲的右值都被划分到了纯右值的概念,而由于右值引用而出现的右值就被划分为了将亡值,将亡值有一下两种:函数返回的右值引用 ,转换为右值引用的转换函数的表达式(std::move)。

我们能够用move函数将一个值转换为将亡值,而转换为将亡值之后就能够调用右值版本的函数,就比如我们上面的移动构造。

为什么我们明明返回的是一个ret,ret明明是一个左值,却会调用右值版本的构造函数呢?这就是因为编译器自动识别然后在返回的时候将 ret 转换为了将亡值。那么我们能够自己进行转换吗?

能,也是一样的效果,不过这种事情除非必要,否则还是让编译器自己来干比较好。

将亡值与纯右值在功能上及其相似,都不能做操作符的左操作数,都可以使用移动构造函数和移动赋值运算符。当一个纯右值来完成移动构造或移动赋值任务时,其实它也具有“将亡”的特点。一般我们不必刻意区分一个右值到底是纯右值还是将亡值。

右值引用的价值之一就是弥补左值的缺陷,减少传值返回时的深拷贝,转而调用代价更低的移动构造

那既然有移动构造,肯定也有移动赋值,移动赋值也是swap一下就行了。

左值引用和右值引用都能减少拷贝,但是他们减少拷贝的原理不一样。左值引用是直接起作用,通过给除了作用域不销毁的对象取别名来减少拷贝。

而右值引用则是通过我们的自定义类型实现移动构造和移动赋值,在拷贝的场景中,如果是右值来调用就会进行资源的转移。

所以移动构造和移动赋值并不是真的把传值返回的或者匿名的临时变量的生命周期延长了,而是将我们需要的资源转移出来,延长了资源的生命周期。对应的局部或者临时对象该销毁还是会销毁。

右值引用单独用是没有意义的,一定要配合移动构造或者移动赋值,当识别到参数是右值时,就不会进行深拷贝而是进行资源转移。

还有一点要注意的就是,由于移动构造和移动赋值是要对被操作的对象进行修改的,所以我们不能将参数写成const版本。 同样,如果接受的是一个const的对象,那么调用的还是左值版本的构造拷贝和拷贝赋值。

如果右值引用写成const 参数的版本就没有意义了,不如就让她去调用左值的const版本,都是进行深拷贝,还省了代码。

右值引用的第二个价值就是对于插入右值数据,也可以减少拷贝

什么意思呢?比如容器中存的是我们的自定义类型对象,我们一般在插入的时候都是直接构造一个匿名对象来进行插入,但是匿名对象进行插入的时候,是要进行一次拷贝的,将对象深拷贝出一个新的对象放在容器中。

那么有了移动构造之后,由于我们的匿名对象本身就是一个天然的右值,都不需要编译器识别和转化,直接就能调用移动构造,将资源转移给容器中的新对象,减少了一次深拷贝。

每一次这样创建匿名对象进行插入都能减少一次深拷贝,这效率还是能提升不少的。

这种插入方式也是一样的,编译器还是会自动构造一个匿名对象进行传参。相当于类型转换。

右值引用是右值还是左值呢?

我们从之前的右值引用和const左值引用的区别就能看出来,右值引用本身就是一个左值,但是右值引用不是右值的别名吗?我们说了右值是无法取地址的,同时右值一般没有名字的,因为右值是存储在我们没有权限访问或者类似于匿名对象这种我们无需知道地址的对象,但是当我们对其进行引用的时候,其实就相当于赋予了他名字,那么在我们能访问的内存中就会给他开辟一段空间存储起来,或者如果是匿名对象的话就直接变成了有名对象,总之就是会存储到特定的位置被引用,但是她所引用的对象我们还是当成右值来看。

既然右值引用本身是左值,那么我们下面的调用

所以我们就能看出来,传右值引用的时候,那么他调用的就是左值版本。

但是如果我们有这种场景呢?

void func(mystring::string& s)
{
	cout << "左值引用" << endl;
}
void func(mystring::string&& s)
{
	cout << "右值引用" << endl;
}

void func1(mystring::string& s)
{
	func(s);
}

void func1(mystring::string&& s)
{
	func(s);
}

int main()
{
	func1(mystring::string("aaaa"));
	return 0;
}

虽然我们传的是一个右值,第一次匹配进去的也是右值引用的版本,但是如果要接着把这个参数往下传呢?那不就变成传左值了?

具体的场景,比如我们实现vector的右值版本的 oush_back ,而 push_back 实际上是要调用insert进行插入的,那么不就变成了调用左值引用版本的insert了,那么就会进行深拷贝。

但是我们期望的是去调用右值版本的 insert ,这时候要怎么办呢?

		void push_back(T&& val)
		{
			insert(end()-1,val);
		}

不管怎么样,当我们在push_back中调用insert的时候,val已经是一个左值了,那么就会匹配左值引用的insert。 怎么解决呢? 我们目前来说好像还就只能够在 val 的前面用 move 将其强制转换为右值传给 insert 。当然这里还有一些其他的问题,一会就能讲到。

		void push_back(T&& val)
		{
			insert(end()-1,(move)val);
		}

这里要注意的是,就算我们传的是 move 之后val 给insert ,调用insert的右值引用版本,但是在insert的栈帧中,对于他而言,接收到的形参又是一个左值了 ,最终进行 *pos =val 的时候,由于val是左值,那么调用的就还是 string 的左值版本的拷贝构造或者拷贝赋值,所以我们在真正去构造或者赋值的时候,也是需要将其 move 一下,这样才能调用到 string 的移动构造或者移动赋值。

*pos =move(val);

模板的万能引用和完美转发

就比如我们上面的代码,如果vector的push_back既实现一个左值引用版本又实现一个右值引用版本,同样,insert也需要实现左值引用版本和右值引用版本,除了接收的参数类型不同,其他的代码都是一样的,这样代码就冗余了

那么C++11是怎么解决这个问题的呢?模板的万能引用

我们可以看一下这样的模板

template<class T>
void func(T&& x)
{
	cout << x << endl;
}

我们看着好像他就是一个参数为右值引用的函数,而我们也知道,右值引用无法引用左值,那么是不是意味着这个函数只能传右值进行调用,而不能传左值呢?

	func(1);
	int a = 0;
	func(a);

但是实际测试下来我们会发现,左值也能作为参数传给他,右值也能传给他,难道是编译器将左值自动转换为右值了?并不是,右值引用不能直接引用左值这个语法就直接打破了这种说法,何况这个a也不是要被销毁的值。

我们可以把模板去掉来验证一下编译器在这种场景是不会自动转换的,因为没有道理。

那么为什么加上模板之后就既能引用左值也能引用右值了呢?

如果 T&& 这种写法出现在函数模板的参数列表中,他所代表的不是右值引用,而是万能引用

这是一种C++11新增的语法。他是万能引用所以编译器会在调用这个函数的时候根据传过去的参数类型来进行实例化,推演出 T 是什么。

它既可以接收左值引用,右值引用,还可以接收const左值引用和const右值引用。

	func(1); //const int&&  --> T : const 
	int a = 0;
	func(a);//int&   --> T : int  引用折叠
	func(move(a)); //int&&  --> T : int 
	const int b = a;
	func(b); //const int&  --> T : const int 引用折叠
	func(move(b)); //const int &&  --> T : const int 

万能引用接收左值的时候,相当于从 && 变成了 & ,我们称之为发生了引用折叠

那么我们的 push_back 和 insert 就不需要只是由于参数类型的不同就写出两个版本了。

因为这两个函数是类模板的成员函数,他们天生就是函数模板,所以我们也可以写成万能引用的形式。

但是这样就解决问题了吗?我们先来看一个简单的类似的模型:

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

template<class T>
void func(T&& x)
{
	print(x);
}

int main()
{
	func(1);
	int a = 1;
	func(a);
	return 0;
}

虽然我们的push_back 能够接收左值和右值,insert也能够接收左值和右值,但是,如果我们调用的是 push_back ,而他还需要讲 x 传递给insert来进行插入。 而在 push_back 的栈帧中,x 不管是左值引用还是右值引用,它都是一个左值了,那么传递给 insert的就是一个完完全全的左值,那么不管我们传给 push_back 的是左值还是右值,最终调用的就都是 insert 的左值版本。

那么我们要怎么解决呢?

我们前面直接实现两个版本,虽然代码冗余了,但是我们在右值版本的push_back中,我们传给insert的参数是 move 之后的参数,那么就是去调用 insert的右值引用版本 ,在insert中使用*pos=move(val)这样就解决了。但是在这里,我们能够直接使用move来强制转换为右值传给insert和string的拷贝或者赋值吗?不能,因为我们的push_back或者insert有可能接收到的原本的参数就是一个左值,可能本意就是要进行一次拷贝构造,这个左值在我们的函数中还有其他用处,那么如果强制转换为右值的话,我们外部的这个左值的资源就被转移了,那么我们外部再使用的话就会出问题。

那么这时候要怎么做呢?

C++11提供了完美转发来解决这一类问题。

完美转发能让我们的引用传递给下一层还能保持在这一层的属性。

我们可以理解为,如果是左值引用,使用完美转发的话就以左值的形式传给下一层,如果是右值,就以右值的形式传给下一层。

完美转发怎么用呢?很简单, forward<T>()   ,在括号中放我们要保持属性的参数就行了。

	print(forward<T>(x));

注意,如果是完美转发,这里的T不要换成具体的类型,就写成T就行了,本就是让编译器去推导的。

那么有了完美转发之后,容器中的左值和右值的传参问题也解决了。

虽然我们目前把传参的问题解决了,但是我们发现在中间位置或者头部进行插入的时候,挪数据的时候还是要深拷贝,效率还是不高。

在我们之前写的挪数据时,无非就是不断地拷贝赋值以及一次深拷贝构造(最后一个位置是一次拷贝构造),我们也可以将这些拷贝赋值和拷贝构造优化成移动构造和移动赋值,因为毕竟我们并不会把这些资源给析构掉,只是转移位置而已,既然只是转移位置,那么移动赋值和移动构造也就够用了,而不需要用深拷贝。

当然,如果我们存的数据个数类型是const,那么就只会走深拷贝,也就是左值引用的版本。

7 新的类功能

C++11之后类的默认成员函数又增加了两个,也就是我们上面写的移动构造和移动赋值

既然是默认成员函数,那么编译器在一些情况下就会默认生成,具体在什么情况呢?

首先,不管怎么说,如果我们自己实现了这两个函数,编译器自然是不会自动生成的,那么还有什么其他的条件吗?

在我们自己没有实现移动构造的前提下,且没有自己实现析构函数,拷贝构造,拷贝赋值这三个成员函数,那么编译器就会自动生成移动赋值。编译器默认生成的移动构造,对于内置类型就完成值拷贝,对于自定义类型就去调用该自定义类型的移动构造如果这个自定义类型没有实现移动构造,就去调用他的拷贝构造

不管怎么说,自定义类型的拷贝构造如果我们不实现,编译器肯定会自动生成一个。但是移动构造则不一定,要满足上面的条件才会自动生成。

那么移动赋值也是类似的

在我们自己没有实现移动赋值的前提下,且没有自己实现析构函数,拷贝构造,拷贝赋值这三个成员函数,那么编译器就会自动生成移动赋值。编译器默认生成的移动赋值,对于内置类型就完成值拷贝,对于自定义类型就去调用该自定义类型的移动赋值,如果没有实现移动赋值,就去调用他的拷贝赋值

其次,C++11允许在类定义的时候给成员变量初始的缺省值,默认生成的构造函数会使用这些缺省值初始化,这个我们在类和对象的时候就讲过了。 缺省值的初始化还是走初始化列表。

C++11针对默认成员函数还增加了两个关键字: default 和 delete

default 就是强制编译器自动生成该默认成员函数。

比如我们定义了一个类,但是没得办法,需要自己实现他的析构函数,那么编译器就不会自动生成他的移动构造了,但是我们又想要移动构造,比如说我们的类中有一个成员是自定义类型的对象,该类已经实现了移动构造,那么我们就可以直接使用 default 让编译器自动生成移动构造。 当然其他的默认成员函数也可以。

	A(A&& a) = default;

而delete的作用则相反,禁止自动生成该成员函数。

比如我们要设计一个类,不能被拷贝,也就是不能调用拷贝构造,在C++11之前有什么办法吗?

首先我们想到的肯定就是拷贝构造私有,但是就算私有了,在类内也是能调用的,比如类内的一个共有的方法调用了拷贝构造,然后这个类又实现了移动构造,那么完全就可以在类内完成一次拷贝,然后通过移动构造间相当于拷贝给外界了。

那么还有一种方法就是,我们自己实现拷贝构造,但是我们在里面用一个 assert(false) 来终止掉程序,那么只要调用了程序就结束。达到了防拷贝的作用,但是assert只在debug版本有用且他的运行时报错,在编译期间检查不出来。

最简单的办法就是我们将拷贝构造的声明写出来,但是不实现该函数。因为我们写了声明,所以编译器不会自动生成了,但是在编译链接的时候,发现这个函数没有实现,所以编译时就报错了。

那么在C++11之后,我们就有一种更简单的方式来禁止拷贝了,就是用delete删除拷贝函数,那么外界只要调用了就会在编译时报错。

	A(const A& a) = delete;

8 lambda表达式

lambda 表达式是为了对标我们之前写过的仿函数,而仿函数又是为了替代函数指针。

比如我们以前使用 sort ,如果要排序的是自定义类型的数据,那么我们还需要写一个仿函数,传一个仿函数对象给 sort 用来进行排序。 

struct myless
{
	bool operator()(const pair<string, string>& p1, const pair<string, string>& p2)
	{
		return p1.first < p2.first;
	}

};


int main()
{
	vector<pair<string, string>> vstr = {{ "sort","排序" }, { "left","左" }, { "right","右" },{"return","返回"}};

	sort(vstr.begin(),vstr.end(),myless());

	for (auto& p : vstr)
		cout << p.first << ":" << p.second << endl;

	return 0;
}

但是仿函数写起来还是比较复杂,比如我们需要两种排序方式,那么就需要写两个仿函数,更多的,比如我们常看的购物网站,商品的排列方式更多,有按价格从高到低和从低到高排序,有销量从高到低,等等排序的规则,难道每一个都要写一个仿函数吗?

所以其实仿函数在有些场景不是很方便,于是C++11就从别的语言中引进了 lambda 表达式。比如上面传的仿函数对象我们就可以换成一个lambda表达式。

	sort(vstr.begin(), vstr.end(), [](const pair<string, string>& p1, const pair<string, string>& p2) 
									{
										return p1.first < p2.first; 
									});

他的效果和我们传一个仿函数对象是一样的,但是我们不需要特意为这一个排序规则而专门去定义一个仿函数类,简单了很多。

lambda 表达式的语法

[ capture-list ] ( parameters ) mutable ->return-type { statement }

每一个部分是什么意思呢?

1 [ capture -list ] 捕捉列表:捕捉列表是 lambda 表达式中必须写的,但是捕捉列表可以为空,捕捉列表总是出现在 lambda 函数的开始位置,编译器会根据 [ ] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用

2 ( parameters ) 参数列表:与普通函数的参数列表一致。如果我们不需要传参数,也可以将 () 一起省略

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

4 ->returntype : 返回值类型,我们使用 -> + 返回值类型 , 用追踪返回形式声明函数的返回值类型,没有返回值时这部分可以省略。如果返回值很明确的情况下,也可以省略这个部分,可以让编译器根据返回的类型自动推到。 但是如果返回的时候需要强制转换等就需要显式声明。

5 { statement } :函数体 ,就跟我们写函数一样,可以定义变量,可以使用传过来的形参等,同时,在lambda 的函数体中,还可以使用所有捕捉的变量

那么最简单的 lambda 表达式就是 [ ]{ } ,他无参无返回,什么也不干

其实 lambda 只是我们写起来比仿函数简单了,本质上,编译器在底层还是会将其处理成一个仿函数对象来传参,lambda 的本质就是一个可调用的对象,和仿函数对象没区别

如何证明呢?我们可以用一个变量定义出 lambda 对象,然后使用 typeid 来查看其类型。

	auto func = [](const pair<string, string>& p1, const pair<string, string>& p2) 
													{
														return p1.first < p2.first;
													};

	cout << typeid(func).name() << endl;

	sort(vstr.begin(),vstr.end(),func);

我们发现lambda的类名是一个lambda_ 再加一串字符组成的,由于这是编译器在底层自动生成的,所以对于我们用户而言其实可以叫做匿名类型,不过还好我们能够使用 auto 让编译器自己推到类型来接收。

之后我们将该对象传给sort 也是可以完成排序的,因为它本质上就是一个仿函数的对象,只不过这个仿函数类是编译器通过我们的lambda表达式自动生成的,他的类名是用算法搞出来,防止重复的。

lambda 的开始部分就是一个捕捉列表,她能够将上下文中的数据捕捉到函数中供我们使用,类似传参,但是不是传参,而是直接捕捉。如何捕捉呢?我们只需要在方括号中将我们要捕捉的变量名或者对象名写上就行了,如果要捕捉多个对象可以用逗号隔开。

	int a = 10;

	auto func = [a]() {a++; };

	func();

但是我们这样写的时候会出现一个报错

在报错信息中我们能知道两个要素:

1 默认的捕捉方式是复制捕获,其实也就是拷贝捕捉

2 默认的lambda是const 的,无法修改通过靠被捕捉的变量。

这就有点类比函数的传值传参,同时还是使用const来接收的。

我们如何让拷贝捕获的变量可修改呢? 不要忘了我们的 mutable 修饰符,使用该修饰符之后,我们拷贝捕获的变量就可以被修改了。

	auto func = [a]()mutable{a++; };

当然由于是拷贝捕捉,所以修改函数体中的a不会影响 父作用域 中的a。

如果我们想要函数体中捕捉的变量和父作用域中的变量是同一个,可以使用 引用捕捉,引用捕捉怎么写呢?

	int a = 10;
	int b = 20;
	auto func = [&a, &b]()mutable {a++; b--; };

在捕捉列表中使用 & 加上我们要捕捉的变量或对象名就行了。注意这里的 & 不是取地址的符号,而是表示引用捕捉,也就是在lambda捕捉到的变量是父作用域变量的别名。

那么如果我们想要将父作用域的所有变量都捕捉进来要怎么办呢?难道一个一个写?有简单的方式

比如我们使用 = ,就代表着将父作用域的变量全部拷贝捕捉

	int a = 10;
	int b = 20;
	
	if (a == 10) 
	{
		int x = 100;
		auto func = [=]()mutable {cout << "a:" << a << "   b" << b << "   x" << x << endl; };
		func();
	}

注意,捕捉父作用域的所有变量并不只是捕捉 lambda 定义的这一个代码块中定义的变量,只要在他定义的位置可以访问到的变量都可以捕捉进去。

那么与此同时,我们可以直接使用 & 将父作用域的所有变量引用捕捉

		auto func = [&]()mutable {cout << "a:" << a++ << "   b" << b++ << "   x" << x << endl; };

我们也可以传值捕捉和引用捕捉配合使用,比如我们这样写:

	auto func = [=, &a]()mutable {a++; b++; c++; };

表示 除了 a 变量引用捕捉,其它的全部变量都拷贝捕捉。

也可以这样:

	auto func = [&,a]()mutable {a++; b++; c++; };

这样就表示除了 a 变量拷贝捕捉,其他父作用域的全部变量都引用捕捉。

但是我们 不能 这么写

	auto func = [=,a]()mutable {a++; b++; c++; }; //报错
	auto func = [&,&a]()mutable {a++; b++; c++; }; //报错

因为 这里的 a 本身就和 = 是匹配的, &a 也本身就和 &是匹配的,多此一举的话编译器会报错。

如果是在类的成员函数内定义的 lambda 表达式 ,使用 = 或者 & 捕捉的话,会将this指针也一并捕捉进去。

与多线程相关:

如果我们传给线程的是一个仿函数的对象,同时对象需要引用传参时,如果这样写:

struct func
{
	void* operator()(int& a)
	{
		while (a)
		{
			cout << a-- << endl;
		}
		return nullptr;
	}
};




	int a = 10;
	int b = 100;
	thread t1(func(),a);
	t1.join();
	cout << a << endl;

编译器是会报错的,

我么需要在传引用的参数前面加上 ref 来标识

	thread t1(func(),ref(a));

同样的,我们传 lambda 表达式作为线程函数的时候也是一样,如果参数要的是引用,我们传的时候也要加上 ref

	thread t1([](int&a) {while (a) cout << a-- << endl; return nullptr; } , ref(a));

但是呢,由于 lambda 有捕捉列表的存在,我们就没必要这么麻烦了,直接在捕捉列表中用引用捕捉就行了。 

	thread t1([&a]() {while (a) cout << a-- << endl; return nullptr; } , ref(a));

那么lambda 有这么多的优点, 是不是就可以完全替代仿函数了呢?

不能,我们不要忘了他的最大的一个缺点,就是他的类型 用户无法在编译时就确定,因为他是编译器在运行时自动生成的。

那么在什么场景下我们必须要知道类型呢?比如类模板。 

就好比我们之前使用的  hashfunc 以及 getkeyodval 这样的仿函数,我们在类模板中要接收的是仿函数的类型,而不是仿函数或者lambda对象。同时,这些类型必须在编译期间就能够确定,因为编译器在编译期间就需要进行模板的推演实例化。而像 lambda 这种运行时才能确定 类型的,我们是无法作为类型模板参数的,最多也就作为非类型模板参数传过去。

所以,仿函数在某些方面还是不会被lambda所取代的。

我们说 lambda 本质上其实就是一个仿函数对象,怎么证明呢?我们可以对比一下双方调用时的汇编。

struct func
{
	void operator()()
	{
		int a = 1;
	}
};

int main()
{
	func func1;
	auto func2 = [] {};

	func1();
	func2();
	return 0;
}

我们可以看到他们底层都是在调用各自的 () 的重载。

9 可变参数模板

可变参数模板对标的是C语言的可变参数,那么我们在什么地方见过可变参数呢?最经典的就是我们的scanf 和 printf ,

printf 的第一个参数就是格式的字符串,第二个参数就是可变参数列表。

而C++11新增了可以在函数模板和类模板中使用可变参数。

比如在我们的thread的构造函数中

可变参数其实就是一个参数包,前面的 Args 是一个模板参数包,或许我们可以简单理解为一种容器类型,args 是接受的形参的参数包。而Args ... args 则是声明一个参数包。&&则是我们讲过的万能引用。

我们把这种前面有 ... 三个点的参数称为参数包,参数包中可能包含 0 - n 个参数,里面的参数可以是任意类型的。

template<class...Args>
void func(Args...args)
{

}

注意,我们在模板中接受参数包类型的时候是使用  class...Args ,而声明参数包的时候则是 Args...args 。

那么既然参数包有 0-n 个参数,我们怎么依次拿出来呢?

首先我们先扩展一个知识,就是我们如何得知参数包中参数的个数呢?

C++给我们提供的方式是直接使用 sizeof 来获取参数包中参数的个数,但是这里的sizeof使用起来又非常的奇怪,需要这样使用:

	sizeof...(args);

这跟我们以前的使用方式完全不同,当然功能也完全不同。 以前的sizeof是一个关键字,可以加括号也可以不加括号,用于求参数或者类型的大小。 而这里的 sizeof 则是用来求参数包中的参数个数,而且,它使用是 首先要在sizeof后面加上三个点  ... ,然后再带上括号,括号里面放我们要求的参数包。

这里的括号不能省略。

那么我们要怎么把参数包里面的参数一个一个拿出来呢?

如果按照我们传统的逻辑,既然我们能够使用sizeof求出参数包的个数,那么是不是能够直接使用  [ ]将参数取出来呢? 想象很美好,但是现实却很骨感。 C++委员会并没有提供这种方便简洁好理解的的方法,语法上是不支持的。 

我们有两种方式来讲参数包中的参数解析出来:

1 利用递归的思维 

怎么玩递归思维呢?首先,我们要知道的是,参数包的参数个数可能是 0 个,或者我们逐个提取,最终也会提取到 0 个,那么这就是我们的结束条件。 而我们每一次要提取一个,但是我们有不确定参数包中第一个参数的类型,所以我们需要模板类型来完成这样的递归。

void func() //结束条件
{
	cout << endl;
}

template<class T ,class...Args>  //在函数模板中进行递归解析参数包
void func(T x ,Args...args)
{
	int cnt = sizeof...(args);
	cout << cnt << endl;
	cout << x << endl;
	func(args...);
}

这样做的逻辑是什么呢? 我们每次进入一个 模板的 func ,都会将第一个参数包中的参数传给 x ,剩下的参数再作为 参数包传给 args ,知道args 的参数个数为0 ,那么调用func的时候,匹配的就是无参的func 了,因为模板的func至少需要一个参数才能匹配。

调用逻辑如下:

注意,我们说的是用的类似递归的思维,实际上不是严格的递归,因为T可能不一样,那么函数就不是用一个,只有函数自己调用自己才是递归。图中也有一些小失误,就是递归调用func的时候应该是 func(args...) ,图中三个点忘写了。

第2种方式就是利用逗号表达式解析参数包

方法很简单,但是理解起来很抽象

template<class T>
void print(T x)
{
	cout << x << endl;
}

template<class...Args>
void func(Args...args)
{
	int arr[] = { (print(args),0)... };
}
	int arr[] = { (print(args),0)... };

这一行为什么能够依次调用 print 去打印参数包中的每一个参数呢?

首先,逗号表达式的运算顺序是从左往右执行,逗号表达式的结果是最后执行的表达式的结果,也就是初始化这个数组的时候会先执行 print(args) ,然后再将其初始化为0,但是我们在后面加了三个点,代表这是一个参数包,而参数包是调用不了 print 的,必须将其参数解析出来,会被展开成:{ (print(arg1), 0),(print(arg2), 0),(print(arg3), 0)... }。同时这里也用到了C++11的列表初始化,初始出来的数组的大小就是参数包中的参数的个数。同时由于都是逗号表达式,所以都会先执行print,然后再将数组的值设为0.

其实理解起来还是很抽象,我们只要知道这里编译器会自动将参数包展开就行了,同时我们了解这种方法就够了。

最后,支持模板的可变参数的意义是什么呢?

我们可以看一下容器新增的emplace 系列接口,比如 list 的emplace_back

他的参数就是一个参数包,我们用它来插入我们自己定义的有提示信息的 string 来试一下。

从这里的提示信息我们都能看出来,emplace_back 没有构造临时对象,而是直接使用 "aaaaa"去构造节点。 而push_back 是先构造了一个临时对象,然后节点采用移动构造的方式来构造。

不管怎么说,push_back 多了一次移动构造。

不过这里要注意一点,由于emplace系列的参数是可变参数,那么他可以接收 0 个参数,也就是说,我们调用它的时候,即使我们没有传参数,他也会插入一个默认构造出来的数据。而push_back如果不传参数则是会报错,因为他的参数是一个万能引用,必须要传参数。

同时,如果是插入类似于 pair 这种需要多个参数进行构造的对象时,push_back必须要使用make_pair构造一个匿名的pair对象才能够传参。 

而emplace 则很方便,可以直接将两个参数传过去,传过去之后,在真正构造节点的时候会依次读取参数包的参数传给 pair 的构造函数。

在这种情况下,emplace 的使用更加简单,同时他的构造的开销还更少(当然更底层的实现的效率我们不清楚)

那么如果我们传三个参数呢?也就是多传参数会怎么样?

会报错,因为本质上 emplace 系列接口最终就是用参数包中的参数去调用对象的构造函数来插入,尾插肯定是插入一个对象,不可能说调用两次构造直接插入两个数据。

如果我们少传也是一样的,因为pair的构造函数只接受两个参数。

这就有点像是懒汉的思想,先不创建对象,等真正要用的时候再来构造对象。

那么emplace 系列能直接传对象吗?也是能的。

	lt.emplace_back(mystring::string("aaaaa"));

如果传的是对象的话,那么就是一次构造和依次移动构造了,和push_back一样。

我们的emplace系列的接收的参数包能够一直往下传,知道要真正插入的时候才使用参数包中的参数进行构造。

所以有人说emplace 的插入比push_back 插入的效率高,这句话确实没错。

对于需要深拷贝的类而言,emplace的插入的效率比push_back 的效率差距很小,因为push_back也就是多一次移动构造,而移动构造其实就是对一些自定义类型比如指针等的浅拷贝,这点多余的消耗相对于构造的时候或者拷贝构造而言基本可以忽略不计。

但是对于不需要深拷贝的自定义类型,emplace的效率就比push_back的效率要高挺多了,尤其是这个类很大时,因为这中间多的依次移动构造其实就是一次浅拷贝,也就是按字节拷贝,如果类的对象很大,那么浅拷贝的代价还是很大的。

或者说深拷贝的类如果没有实现移动构造, 那么就是使用拷贝构造,这时候代价也很大。

当然,如果我们插入的时候传的是左值,他们两种接口是没有区别的,都需要老老实实去调用拷贝构造。

10 包装器

包装器 function 也叫适配器,是对可调用对象的封装,C++中function本质上就是一个类模板

不过他的模板的类型参数传递的方式和我们正常的模板有点不一样。

首先,function 是对我们的可调用对象的封装,比如函数指针,仿函数对象,lambda 等。

function 实例化的方式是 

function<返回值类型(参数)> 定义的对象名

他的参数中,用一个括号将参数包括起来。

我们传统的类模板是一个一个传参数,而function则是可以传参数包,在返回值类型和参数包之间不需要逗号分隔,只需要将参数包括起来,里面的参数之间用逗号隔开。

	function<int(int, int)> f1;
	function<int(int, int)> f2([](int x, int y) {return x + y; });
	function<int(int, int)> f3(add);
	//不能用仿函数匿名对象进行构造
	//function<int(int, int)> f4(Myadd()); //会出现报错,因为会把Myadd识别成函数名 
	Myadd a;
	function<int(int, int)> f4(a);  
    f1=f2;

而他的使用就和仿函数一样,其实f1,f2其实也可以理解为仿函数对象,只不过是库里面给我们提供的类型,同时可以接收不同类型的可调用对象进行包装。

不仅能包装上面的这些可调用对象,function还可以包装类的成员函数,包括静态成员函数和普通成员函数。

struct Myadd
{
	int operator()(int x, int y) { return x + y; };
	static int add1(int x, int y) { return x + y; }
	int add2(int x, int y) { return x + y; }

	
};

int main()
{

	function<int(int, int)> f1(Myadd::add1);
	function<int(int, int)> f2(&Myadd::add1);
	function<int(Myadd,int, int)> f3(&Myadd::add2);

不过要注意的是,如果是包装类的成员函数,包装静态成员函数的话没什么区别,也可以加上取地址符号来传递成员函数的地址。

而如果是普通成员函数,我们则需要注意两个点,一是普通成员函数的第一个参数是this指针,我们要注意 ,但是在传模板参数的时候我们不需要传指针类型,只需要传类类型就行了, 第二个就是在构造的时候要 使用 & 。

那么在调用function 封装 普通成员函数形成的对象时,要如何传参呢?要传 Myadd* 吗?

不需要,我们只需要传一个 Myadd 的对象就行了,在调用的时候会自动转换为 a.add2()。 

	cout << f3(Myadd(),3,2) << endl;

在没有function 之前,我们使用下面的函数模板,

template<class F>
void Func(F f, int x, int y)
{
	cout << f(x, y) << endl;
}


	int a = 1, b =1;
	Func(add, a, b);
	Func([](int x, int y) {return x + y; }, a, b);
	Func(Myadd(), a, b);

该函数模板会被实例化出三份,因为函数指针,Myadd类以及lambda 类是三个不同的类型。

而有了function 之后,我们可以舍弃模板,直接用一个函数来接收这三种类型

void Func(function<int(int,int)> f, int x, int y)
{
	cout << f(x, y) << endl;
}

	Func(add, a, b);
	Func([](int x, int y) {return x + y; }, a, b);
	Func(Myadd(), a, b);

这样就把可调用对象的类型就统一起来了。

那么统一起来有什么用呢?减少了代码量,方便了我们写参数类型,不然就得写成模板,但是写成模板的缺点就是我们不方便看实例化出来的类型是什么。

 bind

bind我们成为绑定,他也是一个适配器,是一个针对函数的参数的适配器。

它可以接受一个可调用对象,然后生成一个新的可调用对象来适应原对象的参数列表。

他有两个功能:

1 调整参数顺序

调整参数顺序我们需要用到一个命名空间 :placeholders ,我们可以不用管这个具体是什么,只要知道它里面能取到我们从外界传进来的参数 ,_1 表示传的第一个参数,_2表示第二个参数,依次类推

int sub(int a, int b)
{
	return a - b;
}

int main()
{
	function<int(int, int)> f1(sub);
	function<int(int, int)> f2 = bind(f1,placeholders::_2,placeholders::_1);

	cout << f1(5, 2) << endl;
	cout << f2(5, 2) << endl;

比如上面的这个代码, 我们使用 bind 来将 f1 调整参数的顺序 转换生成了一个 f2 ,其实就是封装了一层 f1 ,底层调用的时候最终调用的还是 f1 ,只不过在 f2 这一层是将 先传f2接收到的第二个参数,再传 f2 接收到的第一个参数 给f1来调用。

我们把f1称为f2的函数原型,通过bind可以把参数按照自己设定的顺序传给函数原型来执行。

不过这玩意调整参数顺序优点花里胡哨,实际用处其实不是很大。

2 固定参数

固定参数就是我们可以将 函数原型的某一个参数固定,这样我们在使用的时候就不需要穿这个已经固定的参数了。

比如我们的类成员函数,可以这样封装:

	function<int(Sub,int, int)> f1(&Sub::sub); 
	//每次调用必须传一个对象
	function<int(int, int)> f2= bind(&Sub::sub,Sub(),placeholders::_1,placeholders::_2); 
	//固定一个一个匿名对象,我们再调用这个成员函数的时候就不再需要手动传对象了,而是直接使用固定的这个对象

当然这里也可以将f1作为原型,一样的。

这个我们还是很常用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值