[CPP] 左值 lvalue,右值 rvalue 和移動語義 std::move
阿新 • • 發佈:2020-11-10
參考文章:
+ [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"<