右值引用及其作用
??????一、什么是左值和右值?
? ? ? ? 在掌握右值引用前,必須先知道什么是右值,既然有右值,那么肯定有左值。當(dāng)我們在賦值的時候a=b,能夠被放到=號左邊的值即為左值,反則為右值。
? ? ? ? 那么什么值可以作為左值呢?顯然變量肯定是可以作為左值的。什么值不能作為左值呢?顯然常數(shù)、表達式、函數(shù)返回值等,是不能作為左值的,也就是右值。顯然作為左值的都是可以長期保存起來的,對應(yīng)是保存在內(nèi)存中;但常數(shù)、表達式、函數(shù)返回值等都是臨時值,這些值都保存在寄存器中。可以總結(jié):
二、什么是左值引用和右值引用?
? ? ? ? 對左值的引用即為左值引用,對右值的引用即為右值引用。需要注意得是引用是對值得別名,所以不會產(chǎn)生副本,同時引用本身也是變量,所以也是左值。如下:
int &t = a; // a為左值,所以可以賦給左值引用 // int &t1 = 3; // 錯誤 3為一個臨時值,為右值,不能賦給左值引用 // int &&t = a; // 錯誤 a為左值,不能賦給右值引用 int &&t = 3; // 可以 int &&t = -a; // 可以 // int &t = -a; // 不可以 // int &&t1 = t; // 不可以,t本身是左值三、右值引用作用
? ? ? ? 右值引用是C++11新特性,之所以引入右值引用,是為了提高效率。如下面所示:
class A { public:A(size_t N):m_p(new char[N]){}A(const A & a){if (this != &a){delete[]m_p;m_p = new char[strlen(m_p) + 1];memcpy(m_p, a.m_p, strlen(m_p) + 1);}}~A(){delete []m_p;}private:char *m_p = nullptr; };A createA(size_t N) {return A(100); }void func(A a) {// }int main() {func(createA(100));system("pause");return 0; }?這里會導(dǎo)致大量得調(diào)用A得構(gòu)造函數(shù),不考慮編譯優(yōu)化,原本執(zhí)行如下:
從上面可以看出有大量得構(gòu)造、析構(gòu)調(diào)用 ,但是我們做的工作無非就是臨時構(gòu)造一個A(100)給func使用而已。那么可否將臨時A(100)始終一份給到func使用呢?答案就是右值引用。如下:
class A { public:A(size_t N):m_p(new char[N]){}~A(){delete []m_p;}private:char *m_p = nullptr; };A&& createA(size_t N) {return (A&&)A(100); }void func(A&& a) {// }int main() {func(createA(100));system("pause");return 0; }? ? ? ? 我們將臨時A(100)強制轉(zhuǎn)換為了右值引用,同時func形參也是右值引用,也就是將臨時對象延長到了func中,中間避免了其他構(gòu)造和析構(gòu)調(diào)用,提高了效率。
? ? ? ? 注意到我們將A得拷貝構(gòu)造函數(shù)去掉了,因為已經(jīng)用不到。如果原版寫法,去掉拷貝構(gòu)造函數(shù)會崩潰,因為會自動調(diào)用默認(rèn)拷貝構(gòu)造函數(shù),是淺拷貝,中間臨時對象會提前刪除公共內(nèi)存,后面對象再次釋放是就會重復(fù)刪除內(nèi)存導(dǎo)致崩潰。
四、將左值轉(zhuǎn)換為右值(std::move)
? ? ? ? 右值引用只能綁定右值,那是否可以將右值引用綁定到左值呢?可以,一個很簡單的寫法就是強制轉(zhuǎn)換,如下:
int main() {int a = 3;int &&t = (int &&)a;t = 9;cout << a << endl; // a = 9system("pause");return 0; }? ? ? ? 其實,C++11提供了更為優(yōu)雅的轉(zhuǎn)換函數(shù)std::move,std::move(a)無論a是左值還是右值都將轉(zhuǎn)換為右值。如下:
int main() {int a = 3;int &&t = std::move(a);int &&t2 = std::move(3);system("pause");return 0; }五、std::move實現(xiàn)原理(強制轉(zhuǎn)換)
? ? ? ? 可以打開std::move源碼,如下:
template<class _Ty> inlineconstexpr typename remove_reference<_Ty>::type&&move(_Ty&& _Arg) _NOEXCEPT{ // forward _Arg as movablereturn (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));}可以發(fā)現(xiàn),move是將傳入的_Arg值,強制轉(zhuǎn)換為了typename remove_reference<_Ty>::type&&類型,那么typename remove_reference<_Ty>::type是什么呢?接著往下看。
template<class _Ty>struct remove_reference{ // remove referencetypedef _Ty type;};template<class _Ty>struct remove_reference<_Ty&>{ // remove referencetypedef _Ty type;};template<class _Ty>struct remove_reference<_Ty&&>{ // remove rvalue referencetypedef _Ty type;};原來,remove_reference是一個類模板。第一個type類型為傳入類型本身;第二個類模板形參是左值引用,type為去掉的引用類型;第三個類模板形參為右值引用,type為去掉引用的類型。從本身字面意思也可知道remove_reference的作用就是去掉引用得到原本的類型。
? ? ? ? 我們再次回到move。move返回的是typename remove_reference<_Ty>::type&&,原本類型的右值引用。至此move作用就非常清晰了:就是將傳入值強制轉(zhuǎn)換為值原類型的右值引用。
?六、通用引用及其條件
? ? ? ? 如果你足夠仔細的話,你會發(fā)現(xiàn)move形參為_Ty&&,形式上是右值引用,那為什么傳入左值不會發(fā)生錯誤呢?這涉及到通用引用,所謂通用引用就是根據(jù)接受值類型可以自行推導(dǎo)是左值引用還是右值引用。
? ? ? ? 對于形如:
template<typename T> void print(T &&) {}? ? ? ??如果傳入?yún)?shù)是左值則會被推導(dǎo)為print(int&),如果參數(shù)是右值則推導(dǎo)為print(int&&),除了模板外auto &&也有相同效果,如下:
template<typename T> void print(T &&) {}int main() {int a = 9;print(a); // print(int&)print(8); // print(int&)auto &&t = 3; // int &&auto &&t2 = a; // int &system("pause");return 0; }? ? ? ? ?那是不是所有模板函數(shù)形參T&&都是通用引用呢?答案是否定的。條件是:如果聲明變量或參數(shù)具有T&&某種推導(dǎo)類型的類型?T,則該變量或參數(shù)為通用引用,否則就是右值引用(無法傳入左值)。
? ? ? ? 也就是傳入的參數(shù)在編譯時需要推導(dǎo),如果不需要推導(dǎo),則不是通用引用。如下:
template<typename T> class B { public:void print(T &&) {} };int main() {B<int> b;b.print(3); // 為右值引用system("pause");return 0; }因為在編譯print之前print中的參數(shù)已經(jīng)由B<int> b確定了,所以在print編譯時無需推導(dǎo),故B中的T&&為右值引用。如果改為如下:
template<typename T> class B { public:template<typename Arg>void print(Arg &&) {} };int main() {B<int> b;b.print(3); // 為右值引用system("pause");return 0; }? ? ? ? 因為print時函數(shù)模板形參和類模板形參類型時獨立的,故在編譯print時是需要推導(dǎo)的,故Arg&&為通用引用。同理,下面的int&&也不是通用引用,因為類型已經(jīng)確定。
template<typename T> class B { public:void print(int &&) {} };int main() {B<char> b;int t = 0;// b.print(t); // 出錯,print(int&&)為右值引用system("pause");return 0; }?????????通用引用的形式必須是T&&,添加其他修飾都不是通用引用,如下:
template<typename T> void print(const T&&) // 右值引用 { }template<typename T> void print(std::vector<T>&& params) // 右值引用 { }七、引用折疊?
? ? ? ??上面我們知道通用引用雖然形式上是右值引用,但是卻可以接受左值,這是怎么實現(xiàn)的呢?這就是引用折疊。
有如下代碼:
template<typename T> void print(T&& t) { }int main() {int a = 9;print(a);print(9);system("pause");return 0; }?????????print(a)時,因為a為左值,會被推導(dǎo)成print(int& &&t)形式,int& &&t 會被折疊為int &,所以最終形式為print(int &)。(左值被推導(dǎo)為左值引用)
? ? ? ? print(9)時,為9為右值,所以被推導(dǎo)為print(int&& &&)形式,而int&& &&會被折疊為int&&,所以最終形式為print(int&&)。(右值被推導(dǎo)為右值引用)
? ? ? ? 當(dāng)將引用本身
? ? ? ? 引用類型只有兩種,所以折疊形式就是4中,為:T& &,T& &&,T&& &,T&& &&。引用折疊規(guī)則概況為兩種:
- T&& &&折疊為T&&;
- 其他折疊為T&.
? ? ? ? 下面使用typedef驗證引用折疊,如下:
template<typename T> class B { public:typedef T& type;typedef T&& type2; };int main() {int a = 9;B<int>::type t = a; // type->int&B<int&>::type t2 = a; // type->int& &折疊為int&B<int&&>::type t3 = a; // type->int&& &折疊為int&B<int>::type2 t4 = 3; // type2->int&&B<int&>::type2 t5 = a; // type2->int& &&折疊為int&B<int&&>::type2 t6 = 3; // type2->int&& &&折疊為int&&system("pause");return 0; }?八、完美轉(zhuǎn)發(fā)及其意義
? ? ? ? 通用引用既可以接受左值也可以接受右值,但是通用引用本身是左值。如果在函數(shù)模板中繼續(xù)傳遞該值給其他函數(shù),勢必會改變該值的屬性,即都為左值引用。如下:
template<typename T> void _print(T &&t) {cout << (std::is_lvalue_reference<decltype(t)>::value ? "lvalue" : "rvalue") << endl; }template<typename T> void print(T&& t) {_print(t); }int main() {int a = 3;print(a); // lvalueprint(3); // lvaluesystem("pause");return 0; }本來3為右值,傳遞到_print之后變成了左值。整個屬性是被print改變的。那么可否有一種方式以原屬性傳遞呢?答案是std::foward,被稱為完美轉(zhuǎn)發(fā)。如下代碼:
template<typename T> void _print(T &&t) {cout << (std::is_lvalue_reference<decltype(t)>::value ? "lvalue" : "rvalue") << endl; }template<typename T> void print(T&& t) {_print(std::forward<T>(t)); }int main() {int a = 3;print(a); // lvalueprint(3); // rvaluesystem("pause");return 0; }?在print中傳遞給_print的值為std::foward<T>(t),傳遞給_print,接受的值屬性和之前保持一致。所謂完美轉(zhuǎn)發(fā)就是不改變值原本屬性進行轉(zhuǎn)發(fā)。
? ? ? ? 完美轉(zhuǎn)發(fā)有什么意義呢?某個功能對左值和右值處理情況不一致,如果將左值和右值引用當(dāng)作同一種情況使用,可能會會有性能損失。假設(shè)有如下代碼:
class A { public:A() {}A(int a) { m_pa = new int(a); }A(const A &a) {if (this != &a){delete m_pa;m_pa = new int(*a.m_pa);}}~A() { delete m_pa; }int *m_pa = nullptr; };A * _makeA(A &a) {return new A(a); }A * _makeA(A &&a) {A *pa = new A;pa->m_pa = a.m_pa;a.m_pa = nullptr;return pa; }template<typename T> auto makeA(T&& t) {return _makeA(t); }int main() {A a(3);auto t = makeA(a);auto t2 = makeA(A(4));system("pause");return 0; }? ? ? ? 該代碼作用是根據(jù)已有的A對象創(chuàng)建新的A對象指針。因為左值具有延時性,所以根據(jù)左值創(chuàng)建A指針時是將對象中m_pa進行了深拷貝;根據(jù)臨時對象創(chuàng)建A指針時,由于臨時對象由于已經(jīng)創(chuàng)建好了m_pa,沒有必要再創(chuàng)建將臨時對象的m_pa進行深拷貝,只需要將臨時對象的m_pa給到新創(chuàng)建的A即可,同時將臨時對象的m_pa指向nullptr。這樣可以提高性能。
? ? ? ? 但是通過makeA間接傳遞給_makeA之后,都調(diào)用了_makeA(A&),也就是對m_pa進行了深拷貝,這與原本make(A(4))想調(diào)用_makeA(A&&)不一致。
? ? ? ? 如果我們能在makeA中完美轉(zhuǎn)發(fā)t,那么就可以達到要求,這就是std::foward的意義。
九、std::foward原理
? ? ? ?先看下std::foward的實現(xiàn)代碼,如下:
template<class _Ty> inlineconstexpr _Ty&& forward(typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT{ // forward an lvalue as either an lvalue or an rvaluereturn (static_cast<_Ty&&>(_Arg));}template<class _Ty> inlineconstexpr _Ty&& forward(typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT{ // forward an rvalue as an rvaluestatic_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");return (static_cast<_Ty&&>(_Arg));}我們簡化為_foward版本(方便看)?,如下:
template<typename T> T&& _forward(typename remove_reference<T>::type& t) {return static_cast<T&&>(t); }template<typename T> T&& _forward(typename remove_reference<T>::type&& t) {return static_cast<T&&>(t); }foward給出了兩個版本,一個接收左值,一個接收右值。現(xiàn)在我們使用_foward,如下:
void _print(int &t) {cout << "lvalue" << endl; }void _print(int &&t) {cout << "rvalue" << endl; }template<typename T> void print(T &&t) {_print(_forward<T>(t)); }?????????這里有兩個疑問:1)如果我們調(diào)用print(3),_foward會執(zhí)行哪個版本?2)如果我們調(diào)用print(a),因為_foward中強制轉(zhuǎn)換為了T&&, 是否都調(diào)用_print(int&&)版本?
? ? ? ? 問題1:無論print的參數(shù)是左值還是右值,傳遞給print后t都為左值,所以_foward會執(zhí)行左值版本,即第一個版本。
? ? ? ? 問題2:在_foward都將t強制轉(zhuǎn)換為了T&&,按照道理來說,應(yīng)該都會執(zhí)行_print(int&&),不信可以執(zhí)行:
????????int a = 3;
????????_print((int&&)(a));
代碼,發(fā)現(xiàn)確實是調(diào)用了_print(int&&)。既然這樣_foward為什么還能做到完美轉(zhuǎn)發(fā)呢?顯然通過_foward(a)之后的調(diào)用的肯定是_print(int&)版本。在_foward中使用static_cast<T&&>(或者T&&)和int&&結(jié)果不一樣呢?原因是引用折疊。具有推導(dǎo)類型的T&&轉(zhuǎn)換會進行引用折疊。而int&&類型是確定的,不能進行折疊。
? ? ? ? 如果調(diào)用print(3),傳遞給_foward的t為&&類型,然后T&& &&將折疊為T&&,故最終會調(diào)用_print(int&&)版本;如果調(diào)用print(a),傳遞給_foward的t為&類型然后T&& &將折疊為T&,所以最終調(diào)用_print(int&)版本。
參考:
https://covariant.cn/2020/02/25/uniref-in-cpp/
https://avdancedu.com/a39d51f9/
https://blog.csdn.net/weixin_40539125/article/details/89578720?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
????????
總結(jié)
- 上一篇: 物资配送路径问题(一)
- 下一篇: 使用Stream流实现数组与集合的相互转