1. 程式人生 > >[CPP] 左值 lvalue,右值 rvalue 和移動語義 std::move

[CPP] 左值 lvalue,右值 rvalue 和移動語義 std::move

參考文章: + [1] [基礎篇:lvalue,rvalue和move](https://zhuanlan.zhihu.com/p/138210501) + [2] [深入淺出 C++ 右值引用](https://zhuanlan.zhihu.com/p/107445960) + [3] [Modern CPP Tutorial](https://github.com/changkun/modern-cpp-tutorial) + [4] [右值引用與轉移語義](https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/) 刷 Leetcode 時,時不時遇到如下 2 種遍歷 STL 容器的寫法: ```cpp int main() { vector v = {1, 2, 3, 4}; for (auto &x: v) cout< **左值(lvalue, left value)**,顧名思義就是賦值符號左邊的值。準確來說, 左值是表示式(不一定是賦值表示式)後依然存在的持久物件。 > > **右值(rvalue, right value)**,右邊的值,是指表示式結束後就不再存在的臨時物件。 > > C++11 中為了引入強大的右值引用,將右值的概念進行了進一步的劃分,分為:純右值和將亡值。 > > **純右值 (prvalue, pure rvalue)**,純粹的右值,要麼是純粹的字面量,例如 `10, true`; 要麼是求值結果相當於字面量或匿名臨時物件,例如 `1+2`。非引用返回的臨時變數、運算表示式產生的臨時變數、原始字面量、Lambda 表示式都屬於純右值。 C++( 包括 C ) 中所有的表示式和變數要麼是左值,要麼是右值。通俗的左值的定義就是非臨時物件,那些可以在多條語句中使用的物件。所有的變數都滿足這個定義,在多條程式碼中都可以使用,都是左值。右值是指臨時的物件,它們只在當前的語句中有效。 例子: ```cpp int i = 0; // ok, i is lvalue, 0 is rval // 右值也可以出現在賦值表示式的左邊, 但是不能作為賦值的物件,因為右值只在當前語句有效,賦值沒有意義。 // 0 作為右值出現在了”=”的左邊。但是賦值物件是 i 或者 j,都是左值。 (i > 0? i : j) = 233 ``` 總結: + **所有變數**都是左值。 + 右值都是臨時的,表示式結束後不存在,**立即數、表示式中間結果**都是右值。 ### 特殊情況 需要注意的是,字串字面量只有在類中才是右值,當其位於普通函式中是左值。例如: ```cpp class Foo { const char *&&right = "this is a rvalue"; // 此處字串字面量為右值 // const char *&right = "hello world"; // error public: void bar() { right = "still rvalue"; // 此處字串字面量為右值 } }; int main() { const char *const &left = "this is an lvalue"; // 此處字串字面量為左值 // left = "123"; // error } ``` ### 將亡值 **將亡值 (xvalue, expiring value)**,是 C++11 為了引入右值引用而提出的概念 (因此在傳統 C++ 中,純右值和右值是同一個概念),也就是即將被銷燬、卻能夠被移動的值。將亡值表示式,即: + 返回右值引用的函式的呼叫表示式 + 轉換為右值引用的轉換函式的呼叫表示式,例如 `move` 先看一個例子: ```cpp vector foo() { vector v = {1,2,3,4,5}; return v; } auto v1 = foo(); ``` 按照傳統 C++ 的方式(也是我們這些 C++ 菜鳥的理解),上述程式碼的執行方式為:`foo()` 在函式內部建立並返回一個臨時物件 `v` ,然後執行 `vector` 的拷貝建構函式,完成 `v1` 的初始化,最後對 `foo` 內的臨時物件進行銷燬。 那麼,在某一時刻,就存在 2 份相同的 `vector` 資料。如果這個物件很大,就會造成大量額外的開銷。 在 `v1 = foo()` 中,`v1` 是一個左值,可以被繼續使用,但`foo()` 就是一個純右值, `foo()` 產生的那個返回值作為一個臨時值,一 旦被 `v1` 複製後,將立即被銷燬,無法獲取、也不能修改。 而將亡值就定義了這樣一種行為: **臨時的值能夠被識別、同時又能夠被移動**。 在 C++11 之後,編譯器為我們做了一些工作,`foo()` 內部的左值 `v` 會被進行**隱式右值轉換**,等價於 `static_cast &&>(v)`,進而此處的 `v1` 會將 `foo` 區域性返回的值進行移動。也就是後面將會提到的移動語義 `std::move()` 。 個人的理解是,這種語法的引入是為了實現與 Java 中類似的物件引用系統。 ## 左值引用與右值引用 ### 區分左值引用與右值引用的例子 先看一段程式碼: ```cpp int a; a = 2; //a是左值,2是右值 a = 3; //左值可以被更改,編譯通過 2 = 3; //右值不能被更改,錯誤 int b = 3; int* pb = &b; //pb是左值,&b是右值,因為它是由取址運算子返回的值 &b = 0; //錯誤,右值不能被更改 // lvalues: int i = 42; i = 43; // ok, i is an lvalue int* p = &i; // ok, i is an lvalue int& foo(); foo() = 42; // ok, foo() is an lvalue int* p1 = &foo(); // ok, foo() is an lvalue // rvalues: int foobar(); int j = 0; j = foobar(); // ok, foobar() is an rvalue int k = j + 2; // ok, j+2 is an rvalue int* p2 = &foobar(); // error, cannot take the address of an rvalue j = 42; // ok, 42 is an rvalue ``` 那麼問題來了:函式返回值是否只會是右值?當然不是。 ```cpp vector v(10, 0); v[0] = 111; ``` 顯然,`v[0]` 會執行 `[]` 的符號過載函式 `int& operator[](const int x)` , 因此函式的返回值也是可能為左值的。 ### 深入淺出 要拿到一個將亡值,就需要用到右值引用 `T &&`,其中 `T` 是型別。右值引用的宣告讓這個臨時值的生命週期得以延長,只要變數還活著,那麼將亡值將繼續存活。 C++11 提供了 `std::move` 這個方法**將左值引數無條件的轉換為右值**,有了它我們就能夠方便的獲得一個右值臨時物件,例如: ```cpp #include #include using namespace std; void reference(string &str) { cout << "lvalue ref" << endl; } void reference(string &&str) { cout << "rvalue ref" << endl; } int main() { string lv1 = "string,"; // lv1 is lvalue // string &&r1 = lv1; // 非法,右值引用不能引用左值 string &&rv1 = std::move(lv1); // 合法,move 可將左值轉移為右值 cout << rv1 << endl; // string &lv2 = lv1 + lv1; // 非法,非常量引用的初始值必須為左值 const string &lv2 = lv1 + lv1; // 合法,常量左值引用能夠延長臨時變數的生命週期 cout << lv2 << endl; string &&rv2 = lv1 + lv2; // 合法,右值引用延長臨時物件生命週期(通過 rvalue reference 引用 rval) rv2 += "Test"; cout << rv2 << endl; reference(rv2); // 輸出 "lvalue ref" // rv2 雖然引用了一個右值,但由於它是一個引用,所以 rv2 依然是一個左值。 // 也就是說,T&& Doesn’t Always Mean “Rvalue Reference”, 它既可以繫結左值,也能繫結右值 } ``` **為什麼不允許非常量引用繫結到左值?** 一種解釋如下(C++ 真傻逼)。 這個問題相當於解釋下面一段程式碼: ```cpp int i = 233; int &r0 = i; // ok double &r1 = i; // error const double &r3 = i; // ok ``` 因為 `double &r1` 型別與 `int i` 不匹配,所以不行,那為什麼 `const double &r3 = i` 是可以的?因為它實際上相當於: ```cpp const double t = (double)i; const double &r3 = t; ``` 在 C++ 中,所有的臨時變數都是 `const` 型別的,所以沒有 `const` 就不行。 ## 移動語義 先看一段程式碼,熟悉一下 `move` 做了些什麼: ```cpp #include #include using namespace std; int main() { string a = "sinkinben"; string b = move(a); cout << "a = \"" << a << "\"" << endl; cout << "b = \"" << b << "\"" << endl; } // Output // a = "" // b = "sinkinben" ``` 然後看完下面一段程式碼,結束這一回合。 ```cpp template swap(T& a, T& b){ T tmp(a); //現有兩份a的拷貝,tmp和a a = b; //現有兩份b的拷貝,a和b b = tmp; //現有兩份tmp的拷貝,b和tmp } //試試更好的方法,不會生成額外的拷貝 template swap(T& a, T& b){ T tmp(std::move(a)); //只有一份拷貝,tmp a = std::move(b); //只有一份拷貝,a b = std::move(tmp); //只有一份拷貝,b } ``` 個人感覺,`b = move(a)` 這一語義操作,是**把變數 `b` 繫結到資料 `a` 的記憶體區域上**,從而避免了無意義的資料拷貝操作。 下面這一段程式碼可以印證我的這個觀點。 ```cpp #include class A { public: int *pointer; A() : pointer(new int(1)) { std::cout << "構造" << pointer << std::endl; } A(A &a) : pointer(new int(*a.pointer)) { std::cout << "拷貝" << pointer << std::endl; } // 無意義的物件拷貝 A(A &&a) : pointer(a.pointer) { a.pointer = nullptr; std::cout << "移動" << pointer << std::endl; } ~A() { std::cout << "析構" << pointer << std::endl; delete pointer; } }; // 防止編譯器優化 A return_rvalue(bool test) { A a, b; if (test) return a; // 等價於 static_cast(a); else return b; // 等價於 static_cast(b); } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; } /* Output 構造0x7f8477405800 構造0x7f8477405810 移動0x7f8477405810 析構0x0 析構0x7f8477405800 obj: 0x7f8477405810 1 析構0x7f8477405810 */ ``` 對於 `queue` 或者 `vector`,我們也可以通過 `move` 提高效能: ```cpp // q is a queue auto x = std::move(q.front()); q.pop(); // v is a vertor v.push_back(std::move(x)); ``` 如果 STL 中的元素「體積」都很大,這麼做也能節省一點開銷,提高效能。 ## 完美轉發 恕我直言,這個翻譯是個辣雞。英文名叫 Perfect Forwarding . 這是為了解決這樣一個問題:實參被傳入到函式中,當它被再傳到另一個函式中,它依然是一個左值或右值。 ```cpp template void f2(T t){ cout<<"f2"< void f1(T t){ cout<<"f1"<