int main() { grm::string ret1; ret1= grm::to_string(1234); return 0; }
我们可以知道:将局部对象的资源转移给了临时对象,然后将临时对象拷贝赋值给ret1,但是为啥这里打印了拷贝构造和拷贝赋值啊?别忘了,我们用的是现代写法:拷贝赋值是借助拷贝构造实现的。
那当我们实现了移动赋值后呢?
// 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动语义" << endl; swap(s); return *this; }
我们再次运行:
这里依旧是先将局部对象str的资源先转移给临时对象,然后再转移给ret1。
C++11后,STL的所有容器都增加了右值引用的版本:
STL容器中,在insert等成员方法中也增加了右值引用:
其实也很好理解,当我们插入一个临时对象(接就是将亡值的时候)会直接调用右值引用的版本将将亡值的资源给转移走。
所以我们可以总结出:其实左值引用和右值引用本质上都是减少拷贝,提高效率。但是他们出发的角度不同:左值引用是直接从拷贝出发的,右值引用是可以转移一些将亡值的资源。
有些书上面写着,右值引用可以延长对象的生命周期,其实我觉得不太准确,因为对象的生命周期其实并没有延长,延长的是对象里面资源的生命周期。
1.3 新的类功能
原来C++类中,有6个默认成员函数:
构造函数
析构函数
拷贝构造函数
拷贝赋值重载
取地址重载
const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
如果大家想强制生成默认函数可以用default关键字,强制不生成默认函数可以用关键字delete
2 万能引用与完美转发
2.1 万能引用的基本介绍和使用
首先我们先来看看下面的代码:
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<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); int a; PerfectForward(a); PerfectForward(std::move(a)); const int b = 8; PerfectForward(b); PerfectForward(std::move(b)); return 0; }
我们来看看运行结果:
怎么结果全是左值引用呀❓我们不是也引用了右值的吗❓
其实模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。(有些地方也叫做引用折叠)但是在后续使用中都退化成了左值。
所以我们刚才看见的打印的全部是左值。
假如我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
2.2 完美转发
std::forward<T>(t) //在传参的过程中保持了t的原生类型属性。
比如上面的代码我们可以改为:
void PerfectForward(T&& t) { Fun(forward<T>(t)); }
当我们再次运行时:
这样就保留了上层传入对象的类型。
2.3 完美转发的实际应用场景
我们可以借用下当初模拟实现list的那部分代码:凡是向下传的参数都得forward
,
namespace grm { template<class T> struct ListNode { ListNode* _next = nullptr; ListNode* _prev = nullptr; T _data; ListNode(const T& data=T()) :_next(nullptr) ,_prev(nullptr) ,_data(data) {} ListNode(T&& data ) :_next(nullptr) , _prev(nullptr) , _data(std::forward<T>(data)) {} }; template<class T> class List { typedef ListNode<T> Node; public: List() { _head = new Node; _head->_next = _head; _head->_prev = _head; } void PushBack(T&& x) { //Insert(_head, x); Insert(_head, std::forward<T>(x)); } void PushFront(T&& x) { //Insert(_head->_next, x); Insert(_head->_next, std::forward<T>(x)); } void Insert(Node* pos, T&& x) { Node* prev = pos->_prev; Node* newnode = new Node(std::forward<T>(x)); prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } void Insert(Node* pos, const T& x) { Node* prev = pos->_prev; Node* newnode = new Node(x); prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } private: Node* _head; }; } int main() { grm::List<grm::string> lt; lt.PushBack("1111"); lt.PushFront("2222"); return 0; }
注意上面代码是在模拟实现list的基础上改编了一下,否则太长了不太好看。不过总体思路都是一样的,凡是我们向下传的参数都得完美转发一下。
3 可变参数模板
3.1基本语法
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
3.2递归函数方式展开参数包
// 递归终止函数 template <class T> void ShowList(const T& t) { cout << t << endl; } // 展开函数 template <class T, class ...Args> void ShowList(T value, Args... args) { cout << value <<" "; ShowList(args...); } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
我们如果想打印参数包大小语法形式是这样的:cout << sizeof...(args) << endl;
3.3逗号表达式展开参数包
template <class T> void PrintArg(T t) { cout << t << " "; } //展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
3.4 可变参数模板的应用
那究竟STL在哪些地方用到了可变参数模板呢?
首先我们来看看C++11系列提供的emplace系列。
我以vector为例:
网上有人说使用emplace_back系列的效率是高于push_back系列的,但是其实这种说法是不够全面的,我们下面来分析分析:
如果插入的数据是有资源的右值(比如插入了一个string的匿名对象),调用push_back的话是一次构造+移动构造,调用emplace_back的话直接是一次构造,总的来说差别不大。但是如果插入的像日期类的对象,调用push_back的话是一次构造+拷贝构造,调用emplace_back的话直接是一次构造,这样来看效率的确会高一些。所以我们得分情况讨论。
不仅在emplace系列,在C++11提供的线程库中对可变参数模板也有应用,我们先来简单的看看,具体的讲解我将会在讲解线程库的博文中讲解。
好了,今天的讲解就到这里了,如果觉得该文章对你有帮助的话能不能3连支持一下。😘😘😘