完美转发
万能引用
模板中的&&不代表右值引用 而是万能引用 这样它既能接受左值又能接受右值 比如说这样子
template<class T> void PerfectForward(T&& t) { //... }
右值引用和万能引用的区别就是 右值引用需要确定类型 而万能引用会根据传入的类型进行推导 如果传入的实参是一个左值 那么这里的形参t就是左值引用 如果传入的实参是一个右值 那么这里的形参t就是右值引用
下面重载了四个func函数 这四个func函数的参数分别左值引用 const左值引用 右值引用和const右值引用
我们在主函数中使用完美引用模板函数来调用func函数
代码表示如下
但是我们最后发现 不管传入何种类型 最后调用的都是左值引用而不是右值引用
这是为什么呢?
因为只要右值经过一次引用之后右值引用就会被储存到特定位置 这个右值就可以被取地址和修改了 所以在经过一次参数传递之后右值就会退化为左值 如果我们想要让他保持右值的属性 这个时候就要用到完美转发
完美转发保持值属性
要想在参数传递过程中保持其原有的属性 需要在传参时调用forward函数
代码表示如下
template<class T> void PerfectForward(T&& t) { Func(std::forward<T>(t)); }
经过完美转发之后 再次调用PerfectForward函数调用func就会各自匹配到对应的函数了
完美转发的使用场景
下面提供一个简单的list类 分别提供了左值引用和右值引用的接口函数
template<class T> struct ListNode { T _data; ListNode* _next = nullptr; ListNode* _prev = nullptr; }; template<class T> class list { typedef ListNode<T> node; public: //构造函数 list() { _head = new node; _head->_next = _head; _head->_prev = _head; } //左值引用版本的push_back void push_back(const T& x) { insert(_head, x); } //右值引用版本的push_back void push_back(T&& x) { insert(_head, std::forward<T>(x)); //完美转发 } //左值引用版本的insert void insert(node* pos, const T& x) { node* prev = pos->_prev; node* newnode = new node; newnode->_data = x; prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } //右值引用版本的insert void insert(node* pos, T&& x) { node* prev = pos->_prev; node* newnode = new node; newnode->_data = std::forward<T>(x); //完美转发 prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } private: node* _head; //指向链表头结点的指针 };
下面定义一个list对象 储存我们之前实现的list类 我们分别传入左值和右值调用不同版本的push_back函数
shy::list<shy::string> lt; shy::string s("1111"); lt.push_back(s); //调用左值引用版本的push_back lt.push_back("2222"); //调用右值引用版本的push_back
我们可以发现运行结果如下
- 我们在实现push_back的时候复用了insert的代码 对于左值引用的insert函数来说 它会先new一个节点 然后将对应的左值赋值给这个节点 调用赋值运算符重载 又因为赋值运算符重载本质上复用了拷贝构造 所以会打印出来两行文字
- 对于右值版本的push_back函数 它复用了insert的代码 对于右值引用的insert函数来说 它会先new一个节点 然后将对应的右值赋值给这个节点 调用移动构造来进行转移资源
- 这其中调用函数传参的时候多处用到了 完美转发 这是因为如果不使用完美转发就会让右值退化为左值 最终导致多一次深拷贝 从而降低效率
这里演示一下 如果不使用完美转发会是什么样子的
我们发现这里就变成了两次深拷贝
这就是完美转发的使用场景
如果我们想要保持右值的属性 每次传参的时候就必须要使用完美转发
这里还有一点需要注意的是 实现的list类实例化之后参数T&&就不再是万能引用而是右值引用了 因为这个时候它的类型已经确定了
如果想要使用万能引用而不是右值引用需要像上面那样不确定具体类型 在使用时进行推导
与STL中的list的区别
如果将刚才测试代码中的list换成STL当中的list
- 调用左值版本的push_back插入节点时 在构造结点时会调用string的拷贝构造函数
- 调用右值版本的push_back插入节点时 在构造结点时会调用string的移动构造函数
而我们实现的list代码却使用的是赋值运算符重载和移动赋值
这是因为我们是使用的new操作符来申请空间 new操作符申请空间之后会自动调用构造函数进行初始化
而初始化之后就只能使用赋值运算符重载了
而STL库中使用空间配置器获取内存的 因此在申请到内存后不会调用构造函数对其进行初始化 是后续用左值或右值对其进行拷贝构造 所以会产生这样子的结果
如果我们想要达到STL中的效果 我们只需要使用malloc开辟空间 然后使用定位new进行初始化就可以