Loading... 右值引用(Rvalue Reference)是C++11的新特性,它实现了 **转移语义(move)** 和 **完美转发(perfect forwarding)**,主要目的分别是: - 减少两个对象交互时**不必要的拷贝构造**,提升效率 - 简洁明确定义**泛型**函数 > C++中的变量要么是左值、要么是右值。通俗的左值定义指的是⾮临时变量,⽽右值指的是临时对 象。左值引⽤的符号是⼀个&,右值引⽤是两个&& 绑定到右值的引用,用`&&`来指定。右值引用只能绑定到要销毁的对象。 ```cpp int var = 42; int &l_var = var; int &&r_var = var; // 错误:不能将右值引⽤绑定到左值上 int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上 ``` ## 转移语义 借此可以实现**转移语义**,即**从右值中直接拿数据过来初始化或修改左值, ⽽不需要重新构造一个临时左值后再析构右值。**简单来说解决的是各种情形下**对象的资源所有权转移**的问题。 转移语义可以解决以下问题: **① 用右值参数** ```cpp // 在实现转移语义之前 MyString a = new MyString(b + "dafs"); // 成功,const &的形参接收了右值参数,但是调用了额外的拷贝构造函数,生成了一个临时变量,销毁临时变量还调用了一次析构函数 // 实现转移语义之后 MyString a = new MyString(b + "dafs"); // 成功,会调用1次移动构造函数 ``` 上述提到移动构造函数格式如下: ```cpp MyString(MyString && str) { _len = str._len; _data = str._data; // 从右值中获取数据 str._len = 0; str._data = NULL; // 销毁右值 } ``` 顺便说一下,C++内置的函数`std::move()` 可以将一个左值变量转为右值。(后面会详细介绍) 在实现移动构造函数之前,`MyString a = move(b) ` 只会调用拷贝构造函数(如果实现了的话,否则只是复制成员变量)。 而实现了移动构造函数之后,会调用移动构造函数(move(b)被当做右值)。 **② 按值返回** 在有移动语义之前,当一个函数需要返回一个容器对象时,为了不触发额外的拷贝构造,通常会这样写: ```cpp void str_split(const string& s, vector<string>* vec); // 一个按值语义定义的字符串拆分函数 ``` 这样vec就必须在外部被定义,很不方便。 而有了移动语义就可以这样: ```cpp vector<string> str_split(const string& s) { vector<string> v; // ... return v; // v是左值,但优先移动,不支持移动时仍可复制。 } ``` 这样对于unique_ptr很方便。因为**unique_ptr并不支持拷贝**,但是可以用转移语义来转移所有权。在工厂模式中经常会需要这种返回指针的函数: ```cpp unique_ptr<SomeObj> create_obj(/*...*/) { return unique_ptr<SomeObj>(new SomeObj(/*...*/)); } ``` 结合上面两个场景,再举一个例子: ```cpp vector<string> str_split(const string& s); // 没有移动语义前 vector<string> v = str_split("1,2,3"); // 返回的vector用以拷贝构造对象v。为v申请堆内存,复制数据,然后析构临时对象(释放堆内存)。 vector<string> v2; v2 = str_split("1,2,3"); // 返回的vector被复制给对象v(拷贝赋值操作符)。需要先清理v2中原有数据,将临时对象中的数据复制给v2,然后析构临时对象。 // 实现移动语义 vector<string> v = str_split("1,2,3"); // 返回的vector用以移动构造对象v。v直接取走临时对象的堆上内存,无需新申请。之后临时对象成为空壳,不再拥有任何资源,析构时也无需释放堆内存。 vector<string> v2; v2 = str_split("1,2,3"); // 返回的vector被移动给对象v(移动赋值操作符)。先释放v2原有数据,然后直接从返回值中取走数据,然后返回值被析构。 ``` **③ 对象存入容器** `vector`的`push_back()`方法有着两种定义: ```cpp void push_back( const T& value ); // (1) void push_back( T&& value ); // (2) ``` 左值调用1右值调用2。如果你要往容器内放入超大对象,那么版本2自然是不2选择。 ```cpp vector<vector<string>> vv; vector<string> v = {"123", "456"}; v.push_back("789"); // 临时构造的string类型右值被移动进容器v vv.push_back(move(v)); // 显式将v移动进vv ``` **④ vector增长扩容** vector频繁插入会导致容量不可避免地增长,移动语义可以带来一点优化。vector扩容时,会开辟新的内存,将数据迁移过去。移动语义之前就是 **复制并删除**,之后只需要**移动**即可。 **⑤ unique_ptr存入容器** 前面提到过unique_ptr无法复制,如果按照左值传递参数,显然是不可以的。有了移动语义我们就可以将其放入各种容器中(转移所有权) **⑥ std::thread的传递** thread也是一种典型的不可复制的资源,但可以通过移动来传递所有权。同样std::future std::promise std::packaged_task等等这一票多线程类都是不可复制的,也都可以用移动的方式传递。 ### std::move函数 move函数的原型定义如下: ```cpp template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast< typename remove_reference<T>::type&& >(t); } ``` 1. 其中`remove_reference<T>` 作用是移除T中的引用 2. `static_cast `用于进行类型转换,将t转换成目标类型(移除了引用的T后再套一个&&,保证是右值类型) 3. 可以看到在参数列表的 t 的类型里,T加上了&&,这个叫 **通用引用**(universal reference),既可以传递右值,也可以传递左值。用到的是 **引用折叠** 原则。简单讲就是: - 只有右值引用的类型在后面加上&& 仍然是右值引用( **X&& && —> X&&** ) - 其余的再后面加&或者&&都是左值引用 (**X&**) 传入的T类型,如果是右值那么 形参t 可以仍然保持右值类型。这样,这个模板函数就可以同时接受左值和右值了 *没有理解没关系,这将在后面的完美转发中再次提及,不妨碍理解move函数的功能。*。 举个例子: ```cpp string s("hello"); std::move(s) => std::move(string& &&) => 折叠后 std::move(string& ) 此时:T的类型为string& remove_reference<T>::type为string 整个std::move被实例化如下 string&& move(string& t) //t为左值,移动后不能在使用t { //通过static_cast将string&强制转换为string&& return static_cast<string&&>(t); } ``` ```cpp std::move(string("hello")) => 折叠后 std::move(string&&) 此时:T的类型为string remove_reference<T>::type为string 整个std::move被实例如下 string&& move(string&& t) //t为右值 { return static_cast<string&&>(t); //返回一个右值引用 } ``` - **注意**:在没有右值引用之前,为了使用临时变量,通常定义const的左值引用,比如const string&;在有了右值引用之后,为了使用右值语义,不要把参数定义为const左值引用,否则,传递右值时将会调用拷贝构造函数 ## 完美转发 完美转发使⽤这样的场景:需要将⼀组参数原封不动地传递给另⼀个函数。原封不动不仅仅是参数 的值不变,在C++中还有以下的两组属性: - 左值/右值 - const / non-const **完美转发**就是在参数传递过程中,所有这些属性和参数值都不能改变。在**泛型函数**中,这样的需求⼗分普遍。 在实际中有这么一个问题: ```cpp void func(int&& t) { cout << "int&&" << endl; } void func(int& t) { cout << "int&" << endl; } template <typename T> void relay(T&& t) { func(t); } int main() { relay(2); } ``` > 运行结果:int& 显然在上述代码中,`func`被调⽤时传入的 t 其实是左值。这是因为在 `relay(t)` 中的 t **已经被赋予了⼀个"名字"**(即使已经在参数列表中加上了通用引用,试图用引用折叠保持其**类型**为右值引用,但是不妨碍其**本身**是一个左值): ```cpp int a = 100; int&& b = 100; int& c = b; //正确,b为左值 int& d = 100; //错误 ``` 为了保证这些属性,泛型函数需要重载各种版本,左值右值不同版本,还要分别对应不同的const关 系,但是如果**只定义⼀个右值引⽤参数的函数版本,这个问题就迎刃⽽解了**: **使用方法**: 1. 在模板参数列表中使用T&& (**通用引用**,既可以传递左值,也可传递右值) 2. 在函数中使用 `std::forward()` 转发变量 ```cpp void func(int&& t) { cout << "int&&" << endl; } void func(int& t) { cout << "int&" << endl; } template <typename T> void relay(T&& t) { func(forward<T>(t)); } int main() { relay(2); } ``` > 输出:int&& ### 通用引用 所用到的正是上文提到的 **引用折叠**: 1. **T& & –> T&** 2. **T&& & –> T&** 3. **T& && –>T&** 4. **T&& && –> T&&** 简单讲就是: - 只有右值引用的类型在后面加上&& 仍然是右值引用( **X&& && —> X&&** ) - 其余的再后面加&或者&&都是左值引用 (**X&**) 所以在模板函数的参数中使用`T&&`便可以保持原本实参的属性类型,而且**C++的自动类型推导也会据此得到一个正确的实例化的模板**。 ### forward函数 ```cpp template<class _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return (static_cast<_Tp&&>(__t)); } ``` 传入的 `_Tp` 类型,如果是右值那么返回值 `_Tp&&` 可以仍然保持右值类型,反之亦然。 ## 参考 [如何评价 c++11 的右值引用(rvalue reference)特性?](https://www.zhihu.com/question/22111546/answer/30801982) [c++ 之 std::move 原理实现与用法总结](https://ppipp.blog.csdn.net/article/details/84644069) [C++右值引用(std::move)](https://zhuanlan.zhihu.com/p/94588204) [C++函数模版参数为什么是T&&?](https://www.zhihu.com/question/449124435) 最后修改:2021 年 12 月 25 日 03 : 58 AM © 允许规范转载