C++之右值引用与完美转发与可变参数模板
左值引用和右值引用的概念
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
那么到底什么是左值什么是右值呢?
难道说赋值符号左边的是左值,在赋值符号右边的就是右值吗?——错的!
赋值符号左边的是左值这句话是对的,但是后面的那句话就是错的!
或者有的说法是能修改的就是左值,不能修改的就是右值——这也是错的!
int main() { //赋值符号左边的就是左值 int* p = new int(0); int b = 1; const int c = 2;//c也是一个左值!所以无法修改的就是右值这句话也是错的! //下面的都是左值引用! int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p; //p , b ,c都是左值!但是可以在右边出现! //所以赋值符号右边的是右值这句话也是错的! return 0; }
==所以我们应该如何理解左值还是右值呢?==
==左值就是——一个表达数据的表达式!==(例如变量名,或者**解引用的指针!(例如上面的p是左值! *p是一个表达式!也是一个左值!)**)
==左值我们可以获取它的地址!而且还可以对它进行赋值!所以左值可以出现赋值符号的左边!(也可以出现在右边)==——但是右值绝对不可以出现在赋值符号的左边!
==左值引用就是给左值取别名!==
什么是右值
==右值也是一个数据表达式==——例如:字面常量,表达式返回值,函数返回值(这个不能是左值引用返回)等等
==右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边!==
==右值不能取地址!==
==右值引用就是给右值取别名!==
int func() { int x =1,y =2; return x+y; } int& func2(int& x) { return x; } int main() { double x = 1.1 ,y = 2.2; //这个1.1,2.2就是字面常量! //字面常量是不能取地址的! double z = x+y; //x+y 就是表达式返回值!也是一个右值!(我们无法取x+y的地址!) //x+y就是一个右值表达式! int a = func(); //func()返回的是一个右值!我们无法取func()的地址! //如果返回的是一个左值引用那么就是一个左值! //所以这就是为什么传值返回具有常性的原因!——因为他是一个右值! int&b = func2(a); //func2(a)返回的是一个左值引用!所以func2(a)就是一个左值! //我们可以取func2(a)的地址! //右值引用——就是右值的取别名! int&& rr1 = 10;//字面常量 double&& rr2 = x+y;//表达式返回值 int&& rr3 = func();//函数返回值! //int&& rr4 = func2(a);//这个就会报错!因为func2(a)返回的是一个左值引用! }
左值引用和右值引用的比较
首先我们先思考一个问题既然左值引用能引用左值,右值引用能引用右值!那么——左值引用能不能引用右值?
==这是可以的!但是左值引用不能直接引用右值!要const的左值引用才能引用右值!==
int main() { int a= 10; int& ra = a; //int& ra2 = 10; //这个是不行的!因为10是一个右值! //const引用既可以引用左值也可以引用右值! const int& ra3 = 10; const int& rr4 = a; //因为右值是一些不能修改的的值!所以我们可以用const引用来引用右值! //而且和权限有关系!右值就是就是权限平移 //左值就是权限缩小! return 0; }
==那么右值引用能不能引用左值呢?==——不可以!
int main() { int a = 10; //int&& rra = a; //const int&& rra2 = a; //无论是不是const都无法引用左值! //但是有一个特例! int&& rra = std::move(a);//可以引用move后的左值! return 0; }
==但是可以引用move后的左值!——后面我们会详细讲move有什么作用!==
右值引用的意义
那么右值引用究竟有什么意义呢?看上去好像没有什么用?
首先我要先想一想在以前我们使用引用(左值引用)的意义是什么?——在函数传参和函数返回的时候减少拷贝!
template<class T> void Fun1(const T& x)//const左值引用的出现彻底解决了关于函数传参的问题!无论是左值还是右值都可以接收! { cout << x <<endl; } int main() { int x =10; Fun1<int>(10);//右值可以传 Fun1<int>(x);//右值可以传 return 0; }
==但是左值引用并没有彻底解决传返回值的问题!==
template<class T> const T& Fun1(const T& x) { //...... return x; } //如果出了作用域生命周期没有结束那么就可以用左值引用来返回! template<class T> const T& Fun2(const T& x) { //.... T ret = x; return ret; }//如果是一个函数里面的变量!出了作用域就会被摧毁!那么就不能用左值引用来返回! //左值引用无法解决这个问题!只能传值返回! //虽然语法上允许但是不能使用!
==虽然我们上面的例子看上去没有什么,既然无法引用返回那么传值返回不就可以了么?看起来消耗也不大?——但是如果是下面这个例子呢?==
vector<vector<int>> generate(int numRows) { vector<vector<int>> vv(numRows); for(int i = 0; i < numRows; ++i) { vv[i].resize(i + 1, 1); } for(int i = 2; i < numRows; ++i) { for(int j = 1; j < i; ++j) { vv[i][j] = vv[i-1][j] + vv[i-1][j-1]; } } return vv; }
如果是int类型,那么确实拷贝代价不大!但是如果这是一个vector<vector< int >> 类型返回值呢?那么这个拷贝的代价可就太大了!
**在以前——是使用输出型参数来解决这个拷贝问题!**但是用起来不是那么的舒服!
==所以这就是右值引用的意义之一!——解决左值引用尚未解决的问题!==
那么右值引用是如何解决问题的呢?
//如果是找怎么写的!我们会发现!还是会报错! template<class T> T&& fun(const T& x) { T ret = x; return x; } int main() { int a = 10; int&& ra = fun(a); return 0; }
==那么为什么无法使用呢?首先我们得想明白一件事情——为什么我们不可以用左值引用?明明语法是支持了!因为出了作用域后变量就会被销毁!==
那么难道我们使用了右值引用后这个变量出了作用域就不会被销毁了吗?答案是不对的!==出了作用域这个变量依旧会被销毁!==——右值引用不是怎么使用的!
右值引用的原理与用法
在介绍右值引用的用法之前我们得先看是右值引用的原理是什么呢?
这是我们自己写的一个string类型
#include <iostream> #include <algorithm> #include<assert.h> #include <cstring> namespace MySTL { class string { public: typedef char *iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } string(const char *str = "") : _size(strlen(str)), _capacity(_size) { // cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } // s1.swap(s2) void swap(string &s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 拷贝构造 string(const string &s) : _str(nullptr) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); swap(tmp); } string(string &&s) : _str(nullptr) { cout << "string( string&& s) " << endl; string tmp(s._str); swap(tmp); } // 赋值重载 string &operator=(const string &s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this; } ~string() { delete[] _str; _str = nullptr; } char &operator[](size_t pos) { assert(pos < _size); return _str[pos]; } void reserve(size_t n) { if (n > _capacity) { char *tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } //string operator+=(char ch) string &operator+=(char ch) { push_back(ch); return *this; } const char *c_str() const { return _str; } private: char *_str = nullptr; size_t _size = 0; size_t _capacity = 0; // 不包含最后做标识的\0 }; MySTL::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } MySTL::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } reverse(str.begin(), str.end()); return str; } } int main() { MySTL::string ret = MySTL::to_string(1234); return 0; }
==这样子的一个返回值会调用几次拷贝构造呢?——两次!(如果编译器不进行优化)==
==那么为什么可以合二为一呢?——传值返回如果生成的临时对象比较小例如(4字节,8字节)那么就会被存在寄存器里面,如果比较大就会压在上一个的栈帧==
==如果比较小就会放在寄存器,而不是栈帧!==
这是开启编译器优化后的结果
==我们发现它甚至就是直接一个构造就结束了!==
这是关闭编译器优化的结果——如果你无法看到这个结果请手动关闭编译器优化!
但是如果下面有的编译器可能会发生优化!
int main() { MySTL::string ret; ret = MySTL::to_string(1234); return 0; }
==我这个编译器优化比较激进,一般来说除非是连续的构造!否则编译器是不敢进行优化的!==
MySTL::string ret; int main() { ret = MySTL::to_string(1234); return 0; }
==但是怎么写!编译器就绝对不敢优化了——临时变量依旧存在==
int main() { MySTL::string ret; //或者在中间加一条代码编译器也不敢进行优化! for(int i = 0;i<1;++i); ret = MySTL::to_string(1234); return 0; }
==但是就算是编译器发生了优化!还是会有拷贝构造的发生!==
==还是没有彻底的解决问题!——如是一个大对象拷贝代价还是很大!==
==如果此时我们提供一个右值版本的拷贝构造!==
string(string &&s) :_str(nullptr) { cout << "string(const string& s)" << endl; //..... }
此时给一个左值的对象就会去匹配左值引用版本的拷贝构造!
虽然右值可以匹配const的左值引用版本的拷贝构造!但是有了右值引用版本的的拷贝构造!就会因为更匹配而去匹配右值引用这个版本的拷贝构造!
==有了这个拷贝构造就我们就可以看到一下的现象==——如果读者无法看到该现象请手动关闭的你的编译器优化!
int main() { MySTL::string s1("hello") ; MySTL::string s2(s1) ; MySTL::string s3(MySTL::string("world")); return 0; }
==在右值引用的拷贝构造的时候,s2调用拷贝构造!s3调用有右值引用的拷贝构造!匿名对象就是一个右值==
右值的分类
C++又将右值分为两类!
- 纯右值(内置类型或者内置类型表达式纯右值)
- 将亡值(自定义类型或者自定义类型表达式就是将亡值)
将亡值就是字面的意思——==即将死亡(释放)的值==
那么对于一个将亡值——我们是否有必要进行深拷贝呢?没有任何必要!
string(string &&s)//string&& s就是一个将亡值 :_str(nullptr) { cout << "string(const string& s)" << endl; //..... }
==既然这个对象就要被释放了!但是释放之前又要使用这个资源进行一次深拷贝!那么我们是不是有一个选择?==
==将这个将亡值的资源的所有权进行转移,给那个要进行深拷贝的对象!——那么不就既不用发生深拷贝,又构造出了对象了吗?==
string(string &&s) : _str(nullptr) { cout << "string(const string& s)——移动构造" << endl; swap(s);//这样子我们就完成了所有权的转移! }
==这种方式又叫做——移动拷贝!==
而之所以敢怎么做的原因就是因为有——右值引用的存在!有了右值引用后就可以韩浩的区分左值和右值了!——左值就只会去匹配深拷贝!而右值(将亡值)就回去匹配移动构造!不进行拷贝只进行资源权限的转移!
int main() { MySTL::string s1("hello"); MySTL::string s2(s1); MySTL::string s3(MySTL::string("world")); return 0; }
移动构造不要乱用!——因为移动构造本质是资源所有权的转移!
int main() { MySTL::string s1("hello"); MySTL::string s2(s1); //MySTL::string s3(MySTL::string("world")); MySTL::string s3(std::move(s1));//move可以将左值变成右值 //这就是move的作用! return 0; }
namespace MySYL { MySTL::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } MySTL::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } reverse(str.begin(), str.end()); return str; } } int main() { MySTL::string ret = MySTL::to_string(1234); return 0; }
这是我们上面写的一个代码在开启编译器优化后——是下面的这个结果!
==其实本质就是编译器将左值给优化为了右值!==
这样子传值返回的成本就很低了!
当然只有移动构造是不够了!因为还没有解决赋值的问题!
MySTL::string ret; int main() { ret = MySTL::to_string(1234); return 0; }
==所以我们还得补充一个移动赋值!==
string &operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s);//这样子这个值本身的资源还可以借用s进行释放! return *this; }
有了移动赋值之后——我们就不用担心深拷贝的问题了!
有了移动构造和移动赋值之后!我们就不用再担心传值返回了和赋值!——而这也才是==右值引用的真正用法!==
解决传值返回的是使用移动构造!而不是说直接将返回值改为右值返回!
解决负载是通过移动移动赋值!
==右值引用的都是通过间接的方式来解决==
有一个说法是——右值引用延长了生命周期其实是错误的!右值引用本质是将资源所有权给转移了!对象的声明周期是没有改变的!
有了移动构造和移动赋值就可以极大的提升了效率!
像是上面的这个例子只要vector提供了移动拷贝和移动赋值!那么我们就完全不用担心会出现多次的拷贝从而导致资源的浪费!
vector<vector<int>> generate(int numRows) { vector<vector<int>> vv(numRows); for(int i = 0; i < numRows; ++i) { vv[i].resize(i + 1, 1); } for(int i = 2; i < numRows; ++i) { for(int j = 1; j < i; ++j) { vv[i][j] = vv[i-1][j] + vv[i-1][j-1]; } } return vv;//vv的这个返回值是一个将亡值是一个右值! }
左值引用与右值引用的原理不同
左值引用和右值引用都是为了减少拷贝,但是原理不一样!
左值引用是取别名,直接起作用!
右值引用是间接起作用!是通过移动构造和移动赋值!——在拷贝的场景中,如果是右值(将亡值)那么就直接转移资源!
关于const右值引用和右值引用
为什么要有const右值引用呢?
左值引用可以修改,const左值引用不能修改!
但是==右值本身就是一个不可修改的值!所以为什么要有const右值引用呢?==
int main() { double x =1.1,y = 2.2; int&& rr1 = 10; const double&& rr2 = x+y; rr1++;//这个是可以的! //rr2++;//这个是不可以的! }
==这是为什么?右不是不可以修改的吗?==
是右值是不能取地址的,但是给右值取别名后,会导致==右值被存储到特定位置==,且可以取到该位置的地址
就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇, 这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
==右值不能取地址!但是右值被引用后可以认为变成了左值!==
int main() { double x =1.1,y = 2.2; int&& rr1 = 10; const double&& rr2 = x+y; //下面两个不行,不能取右值的地址! //cout << &(x+y) <<endl; //cout << &10 << endl; //但是可以取右值引用的地址! cout << &rr1 << endl; cout << &rr2 << endl; }
==像是rr1++,其实不是在给10进行++,而是在被存储在特定空间上的值进行++==
为什么啊要这样呢?——如果不这样我们==的移动构造就没有办法实现了!==
string(string &&s) : _str(nullptr) { cout << "string(const string& s)——移动构造" << endl; swap(s);//这个就是在修改右值引用!如果右值引用没有办法修改!那么就无法完成资源所有权的转移! }
当然了读者可能会怎么想!如何还是要开辟一个新的空间存不相当于还是进行了拷贝吗?像是10,存储在特定的空间中,不也是弄了一个int类型的变量进行存储吗?这样子看上去没有节省呀?
==但是如何是对于vector< vector< int >>这种类型的对象!我们在空间中只是存储了这个对象底层的几个指针!(或者用办法实现具体要看编译器的底层实现!)而不是真的将整个资源都存储在那个空间里面!进行所有权的转移本质就是指针的交换!==
STL容器的右值引用的运用
右值引用在插入的运用
右值引用出了在返回值和赋值方面减少了拷贝之外!
对于一些右值数据的插入!也可以减少拷贝!
int main() { list<MySTL::string> lt; MySTL::string str("111111"); lt.push_back(str); lt.push_back(MySTL::string("111111")); lt.push_back("111111"); //在没有构右值引用之前,这几种写法是没有什么区别的! return 0; }
我们要插入一个链表!我们首先就要new一个新的节点!然后进行拷贝构造!最后进行链接!
==但是当我们有了移动构造之后!==
int main() { list<MySTL::string> lt; MySTL::string str("111111"); lt.push_back(move(str)); //我们也可以move将str变为一个将亡值从而调用移动构造! //但是要小心使用! return 0; }
下面的两种写法反倒会更优**!因为下面两种写法都是生成一个右值对象**!可以调用移动构造!将本要被销毁的资源的所有权直接进行转移!上面的是左值对象!所以只能调用左值引用!
万能引用的概念
模板中的&&
我们先看下面这个例子
void func1(int& t) { cout << "void func1(int& t)" << endl; } void func2(int&& t) { cout << "void func2(int&& t)" << endl; } int main() { int x =1; func1(x);//匹配左值 func2(1);//匹配右值 }
int main() { int x =1; func2(x);//但是右值不能匹配左值 func2(1);//匹配右值 }
==但是上面只针对一般的函数!==
template<class T> void perfectForward(T&& t) { } int main() { int x=1; perfectForward(1); perfectForward(x); }
==但是我们发现如果是上面的模板函数那么就不会报错!==
//这个我们一般叫做万能引用!——还有一个说法叫做引用折叠 template<class T> void perfectForward(T&& t) { }
==如果传过来的是一个左值,那么这就是一个左值引用!如果传过来的是一个右值,那么这就是一个右值引用!==
万能引用的使用
int main() { perfectForward(10);//右值 int a = 10; perfectForward(a);//左值 perfectForward(move(a));//右值 const int b = 10; perfectForward(b);//const左值 perfectForward(move(b));//const右值 }
==万能引用所有的引用都能接收!而且那么没有const也可以接受const引用!==
==就是通过折叠来支持万能引用的!==
template<class T> void perfectForward(T&& t) { t++;//那么这个t能不能++呢? }
==如果不上传const的左值和右值那么是可以++的!如果上传了const的左值和右值那么就不可以!因为本质就是实例化出了四个函数!前面两个函数可以支持++,后面两个实例化函数不支持++==
完美转发
通过上面完美引用的性质我们可以写出如下代码
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 = 10; perfectForward(a);//左值 perfectForward(move(a));//右值 const int b = 10; perfectForward(b);//const左值 perfectForward(move(b));//const右值 }
那没事上面的代码会执行那几个Fun呢?
==答案是都执行前两个去了!这是为什么呢?==
原因就出在这个t!
void perfectForward(T&& t) { Fun(t); }
如果t是一个左值就不用解释!肯定是调用前两个!
但是如果t是一个右值!我们上面说过!右值被引用后会被存储在一个特定的空间!——此时这个右值引用可以被修改,取地址!——==所以这个右值引用其实是一个左值!==
==要记住这个概念!右值引用是一个左值!==
那么就不奇怪了!因为右值引用是一个左值所以自然会去调用前面的两个函数!
==那么我们能怎么解决呢?==——因为右值引用是一个左值所以我们move一下吗?
void perfectForward(T&& t) { Fun(move(t)); }
这样子是不行的!如果我们传的不是右值而是左值该怎么办?——move不能解决这个问题!
<img src="https://s2.loli.net/2023/05/11/d6MFgW8reyDxQc3.png" alt="image-20230511160503087" />
修改后的结果!
==有了万能引用之后!t可能是左值也可能是右值!==
为了解决这问题!于是C++引入了新的一个概念——==完美转发==
void perfectForward(T&& t) { Fun(forward<T>(t));//这个就是完美转发! }
==完美转发的作用就是保持它的属性!==
C++11新增默认成员函数
默认成员函数 原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值运算符重载。
默认移动构造
如果你**没有自己实现==移动构造函数==,且没有实现==析构函数== 、==拷贝构造==、==拷贝赋值重载==中的任 意一个。**那么编译器会自动生成一个默认移动构造。
简单的说就是——要么什么都不写!要么就只写一个构造函数!
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,==自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。==
class Person { public: Person(const char *name = "", int age = 0) : _name(name), _age(age) { } private: MySTL::string _name;//默认移动构造回去调用string的移动构造! int _age;//对于内置类型会进行直拷贝! }; int main() { Person s1; Person s2 = s1;//拷贝构造 Person s3 = move(s1);//移动构造 }
==正如看到的一样默认移动构造自定义类型会去调用自定义类型的移动构造!!==
如果我们屏蔽掉我们自己写的string类的移动构造!
==那么默认移动构造就回去调用拷贝构造!==
当我们随便实现三个函数中的一个的时候!那么==默认移动构造==就不会生成
class Person { public: Person(const char *name = "", int age = 0) : _name(name), _age(age) { } // Person(const Person &p) // : _name(p._name), _age(p._age) // { // } // Person &operator=(const Person &p) // { // if (this != &p) // { // _name = p._name; // _age = p._age; // } // return *this; // } ~Person()//我们随便放一个出来 { } private: MySTL::string _name;//默认移动构造回去调用string的移动构造! int _age;//对于内置类型会进行直拷贝! }; int main() { Person s1; Person s2 = s1;//拷贝构造 Person s3 = move(s1);//拷贝构造! }
==默认生成的const左值引用的拷贝构造,可以接受左值,也可以接收右值!==
默认移动赋值运算符重装
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。
也是和默认移动构造一样!——要么什么都不写!要么就只写一个构造函数! 默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)——我们这里就不进行演示了
int main() { Person s1; Person s2 = s1;//拷贝构造 Person s3; s3 = move(s2);//这里就会调用默认移动赋值重载! }
default——强制生成默认函数
==假如我们真的遇到那非要写析构函数 、拷贝构造、拷贝赋值重载呢?——但是其实默认生成的移动构造就已经够用了!那么我们要怎么办呢?——我们可以使用defualt来强制生成!==
class Person { public: Person(const char *name = "", int age = 0) : _name(name), _age(age) { } Person(const Person &p) : _name(p._name), _age(p._age) { } //我们可以看到这样子写其实很麻烦! /*Person(Person &&p) : _name(forward<MySTL::string>(p._name))//要使用move或者forward,否则p._name是左值,不能调用右值引用的构造函数 , _age(p._age) { }*/ //有了default我们就可以怎么写 Person(Person &&p) = default; //这样子就可以生成一个默认移动构造! ~Person() { } private: MySTL::string _name; int _age; }; int main() { Person s1; Person s2 = s1;//拷贝构造 Person s3 = move(s1);//移动构造 return 0; }
可变参数模板
C++中的可变参数模板对标的是c语言中的可变参数列表!
我们在最开始使用的printf就有用到可变参数列表!
template<class ...Args>//可变参数模板! void showlist(Args... args) { } //class ...xxxx 就是用来声明模板参数包! //Args是一个模板参数包! //args是函数形参参数包! //Args... args——这就是声明一个参数包!
C++中也延续了c语言的风格!用三个点来表示!
可变参数模板我们一般不会写!但是在库中经常可以看见!——像是thread的构造函数里面就使用了这个
像是thread的构造函数里面就使用了这个
又像是我们经常可以在每一个可以插入的容器中经常看见的——emplace系列的插入接口!
==我们一起只能传固定的数量的参数!但是有了可变参数包之后!我们就可以传任意个(0-n个)的参数!==
template<class ...Args> void showlist(Args... args) { } int main() { showlist();//不穿也可以! showlist(1); showlist(1,2.2); showlist(1,2.2,"hello"); showlist(1,2.2,"hello",string("world")); }
==因为它是一个模板!所以传什么类型都可以!==
我们可以通过sizeof来获取参数包里面有几个参数!
template<class ...Args> void showlist(Args... args) { cout << sizeof...(args) << endl;//这不是计算args的大小!这是计算包里面有几个参数 //cout << sizeof...(Args) << endl;//使用类型也可以,与上面作用相同!都是计算包里有几个参数! //sizeof后面一定要加上... 否则会报错! }
==那么我们该如何提取参数包里面的内容呢?==
template<class ...Args> void showlist(Args... args) { cout << sizeof...(args) << endl; for(size_t i = 0;i < sizeof...(args);i++) { cout << args[i] << endl;//这是错的!不能像数组怎么用! } } //提取可变参数包的正确写法1! void showlist() { cout << endl; } template<class T,class ...Args>//我们要多加一个模板参数 void showlist(T val,Args... args)//val是用来接收参数包里面的参数的 { cout << val << " "; showlist(args...); } //这样子我们就可提取参数包里面的参数了! int main() { showlist(1); showlist(1,2.2); showlist(1,2.2,"hello"); showlist(1,2.2,"hello",string("world")); }
==看上去是十分的怪异——那究竟是怎么做到的?==
void showlist() { cout << endl; } template<class T,class ...Args> void showlist(T val,Args... args) { cout << val << " "; showlist(args...); } int main() { showlist(1);//这个只有一个参数,所以回去匹配val,然后传0个参数给参数包(我们上面说过参数包可以接收(0-n)个的参数) //然后我们之所以写一个void showlist();就是因为要用到函数重载!当参数包参数个数为0的时候,就会去调用这个函数! showlist(1,2.2); //首先1被val(int类型)接收,2.2被参数包接收! //所以先打印出 1,然后将参数包进行传参 //因为showlist是一个模板首先它会推演出函数!然后再去调用这个函数 //这个新推演出来的函数val(是double类型)接收到2.2,参数包接到0个参数! //然后打印2.2,此时参数包中参数个数是0个!所以回去调用我们上面写的无参的showlist! //上面严格来说不是递归!因为递归都是调用自身!但是模板每一次生成的函数都是不一样的! showlist(1,2.2,"hello"); showlist(1,2.2,"hello",string("world")); }
==我们可以看到其实库里面也是这样子的!可变参数包前面加上一个参数==
//当然了也不一样要使用模板参数! //写法2 template <class T> void PrintArgs(T t) { cout << t << " "; } template <class ...Args> void showlist(Args... args) { int arr[] = {(PrintArgs(args),0)...};//这又是什么东西? cout << endl; } int main() { showlist(1); showlist(1,2.2); showlist(1,2.2,"hello"); showlist(1,2.2,"hello",string("world")); }
int arr[] = {(PrintArg(args),0)...};//这个...的意思就是将参数包展开 (PrintArg(args),0)//首先这是一个逗号表达式!那么逗号表达式就应该取后面的 0 //然后0 取初始化这个数组!也就是说这个数组大小为0 //然后参数包继续展开!就会变成这样(PrintArgs(args),1),拿1取初始化数组! //然后想参数包里面的内容依次传给t //因为后面为1所以要再展开一次! //又变成了(PrintArg(args),0)
//当然了也不一样要使用模板参数! //写法2 template <class T> void PrintArgs(T t) { cout << t << " "; } template <class ...Args> void showlist(Args... args) { int arr[] = {PrintArgs(args)...};//也可以怎么写!不用逗号表达式! //让编译器自己去推!右几个参数就展开几次! //这个数组其实就是一个辅助作用!没有什么用 cout << endl; } int main() { showlist(1); showlist(1,2.2); showlist(1,2.2,"hello"); showlist(1,2.2,"hello",string("world")); }
==STL中也有只用可变模板参数的==
emplace与一般插入的区别
emplace是用万能引用接收的!
Insert是分别写了左值和右值的版本!
int main() { std::list<int> list1; list1.push_back(1); list1.emplace_back(2); //对于内置类型来说,效果是一样的! list1.emplace_back();//对于emplace_back来说,如果不传参数,那么就是默认构造! //int() 就是 0,相当于插入了0 for(auto& e:list1) { cout << e << " "; } // list1.emplace_back(1,2,3,4,5,6,7);//不过虽然说可变参数是支持的!但是编译器还是会报错! }
我们可以看到成功的插入进去了!
//对于内置类型,其实是没有什么性能差别的! //但是对于内置类型! int main() { list<pair<int,string>> list; list.push_back(make_pair(1,"hello")); //或者 list.push_back({1,"hello"}); //但是不可以 //list.push_back(1,"hello"); //上面的都是先构造,再拷贝构造! //如果是右值,那么就是先构造,再移动构造! //但是对于emplace_back,我们可以直接 list.emplace_back(1,"hello"); //相当于将参数一直拿下去然后构造出来!——直接拿参数包里面的参数构造出来! //emplace也只上面的写法 return 0; }
//我们可以用我们自己写的string演示一下!右值的原理与用法哪里 namespace MySTL { class string { public: // 拷贝构造——记得修改一下拷贝构造改成传统写法否则会容易有干扰 string(const string &s) : _str(nullptr) { cout << "string(const string& s) -- 深拷贝" << endl; reserve(s._capacity); strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } private: char *_str = nullptr; size_t _size = 0; size_t _capacity = 0; // 不包含最后做标识的\0 }; } int main() { std::list< std::pair<int, MySTL::string> > mylist; pair<int, MySTL::string> kv(30, "sort"); cout << "====================================================="<<endl; mylist.emplace_back(make_pair(20, "sort"));//插入右值 //这个是直接构造 mylist.emplace_back(kv);//插入左值 //这个是进行深拷贝 cout << "====================================================="<<endl; mylist.push_back(kv);//插入左值 //这个是进行深拷贝 mylist.push_back(make_pair(20, "sort"));//插入右值 //这格式进行构造+移动构造! return 0; }
==我们可以发现在自定义类型的情况下,对于左值,push_back和emplace都是用深拷贝!对于右值,emplace是直接进行构造!而push_back是先构造!再移动构造(如果没有移动构造就是拷贝构造!)==
emplace在右值情况下性能略优一点!其实也没有太大差距**(但是如果没有移动构造!那么性能差距就很大了!因为多了一次深拷贝!)**
其实相比之下emplace对于浅拷贝的类,性能可能会比一般插入更好一点!因为emplace相比之下插入左值就是进行浅拷贝,一般插入就是先构造(浅拷贝),然后再移动构造(移动构造也是浅拷贝)
少了一次浅拷贝,但是因为浅拷贝的代价本身也不大,所以差距也不是很大!