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

參考文章:ios

刷 Leetcode 時,時不時遇到以下 2 種遍歷 STL 容器的寫法:git

int main()
{
    vector<int> v = {1, 2, 3, 4};
    for (auto &x: v)
        cout<<x<<' ';
    cout<<endl;
    for (auto &&x: v)
        cout<<x<<' ';
    cout<<endl;
}

一個困擾我好久的問題是 auto &auto && 有什麼區別?github

左值、右值、純右值、將亡值

首先要明確一個概念,值 (Value) 和變量 (Variable) 並非同一個東西:ide

  • 值只有 類別(category) 的劃分,變量只有 類型(type) 的劃分。
  • 值不必定擁有 身份(identity),也不必定擁有變量名(例如 表達式中間結果 i + j + k)。

定義

左值(lvalue, left value),顧名思義就是賦值符號左邊的值。準確來講, 左值是表達式(不必定是賦值表達式)後依然存在的持久對象。函數

右值(rvalue, right value),右邊的值,是指表達式結束後就再也不存在的臨時對象。性能

C++11 中爲了引入強大的右值引用,將右值的概念進行了進一步的劃分,分爲:純右值和將亡值。優化

純右值 (prvalue, pure rvalue),純粹的右值,要麼是純粹的字面量,例如 10, true; 要麼是求值結果至關於字面量或匿名臨時對象,例如 1+2。非引用返回的臨時變量、運算表達式產生的臨時變量、原始字面量、Lambda 表達式都屬於純右值。this

C++( 包括 C ) 中全部的表達式和變量要麼是左值,要麼是右值。通俗的左值的定義就是非臨時對象,那些能夠在多條語句中使用的對象。全部的變量都知足這個定義,在多條代碼中均可以使用,都是左值。右值是指臨時的對象,它們只在當前的語句中有效。spa

例子:翻譯

int i = 0; // ok, i is lvalue, 0 is rval

// 右值也能夠出如今賦值表達式的左邊, 可是不能做爲賦值的對象,由於右值只在當前語句有效,賦值沒有意義。
// 0 做爲右值出如今了」=」的左邊。可是賦值對象是 i 或者 j,都是左值。
(i > 0? i : j) = 233

總結:

  • 全部變量都是左值。
  • 右值都是臨時的,表達式結束後不存在,當即數、表達式中間結果都是右值。

特殊狀況

須要注意的是,字符串字面量只有在類中才是右值,當其位於普通函數中是左值。例如:

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

先看一個例子:

vector<int> foo()
{
    vector<int> v = {1,2,3,4,5};
    return v;
}
auto v1 = foo();

按照傳統 C++ 的方式(也是咱們這些 C++ 菜鳥的理解),上述代碼的執行方式爲:foo() 在函數內部建立並返回一個臨時對象 v ,而後執行 vector<int> 的拷貝構造函數,完成 v1 的初始化,最後對 foo 內的臨時對象進行銷燬。

那麼,在某一時刻,就存在 2 份相同的 vector 數據。若是這個對象很大,就會形成大量額外的開銷。

v1 = foo() 中,v1 是一個左值,能夠被繼續使用,但foo() 就是一個純右值, foo() 產生的那個返回值做爲一個臨時值,一 旦被 v1 複製後,將當即被銷燬,沒法獲取、也不能修改。

而將亡值就定義了這樣一種行爲: 臨時的值可以被識別、同時又可以被移動

在 C++11 以後,編譯器爲咱們作了一些工做,foo() 內部的左值 v 會被進行隱式右值轉換,等價於 static_cast<vector<int> &&>(v),進而此處的 v1 會將 foo 局部返回的值進行移動。也就是後面將會提到的移動語義 std::move()

我的的理解是,這種語法的引入是爲了實現與 Java 中相似的對象引用系統。

左值引用與右值引用

區分左值引用與右值引用的例子

先看一段代碼:

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

那麼問題來了:函數返回值是否只會是右值?固然不是。

vector<int> v(10, 0);
v[0] = 111;

顯然,v[0] 會執行 [] 的符號重載函數 int& operator[](const int x) , 所以函數的返回值也是可能爲左值的。

深刻淺出

要拿到一個將亡值,就須要用到右值引用 T &&,其中 T 是類型。右值引用的聲明讓這個臨時值的生命週期得以延長,只要變量還活着,那麼將亡值將繼續存活。

C++11 提供了 std::move 這個方法將左值參數無條件的轉換爲右值,有了它咱們就可以方便的得到一個右值臨時對象,例如:

#include <iostream>
#include <string>
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++ 真傻逼)。

這個問題至關於解釋下面一段代碼:

int i = 233;
int &r0 = i; // ok
double &r1 = i; // error
const double &r3 = i; // ok

由於 double &r1 類型與 int i 不匹配,因此不行,那爲何 const double &r3 = i 是能夠的?由於它實際上至關於:

const double t = (double)i;
const double &r3 = t;

在 C++ 中,全部的臨時變量都是 const 類型的,因此沒有 const 就不行。

移動語義

先看一段代碼,熟悉一下 move 作了些什麼:

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string a = "sinkinben";
    string b = move(a);
    cout << "a = \"" << a << "\"" << endl;
    cout << "b = \"" << b << "\"" << endl;
}
// Output
// a = ""
// b = "sinkinben"

而後看完下面一段代碼,結束這一回合。

template <class T> swap(T& a, T& b){
  T tmp(a);  //現有兩份a的拷貝,tmp和a
  a = b;     //現有兩份b的拷貝,a和b
  b = tmp;   //現有兩份tmp的拷貝,b和tmp
}

//試試更好的方法,不會生成額外的拷貝
template <class T> 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 的內存區域上,從而避免了無心義的數據拷貝操做。

下面這一段代碼能夠印證個人這個觀點。

#include <iostream>
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&&>(a);
    else
        return b; // 等價於 static_cast<A&&>(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 提升性能:

// 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 .

這是爲了解決這樣一個問題:實參被傳入到函數中,當它被再傳到另外一個函數中,它依然是一個左值或右值。

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T t){ 
    cout<<"f1"<<endl;
    f2(t);  
    //若是t是右值,咱們但願傳入f2也是右值;若是t是左值,咱們但願傳入f2也是左值
}   
//在main函數裏:
int a = 2;
f1(3); //傳入右值
f1(a); //傳入左值

在引進👆巴拉巴拉的這一套機制以前,即 C++11以前的狀況是怎麼樣的呢?當咱們從 f1 調用 f2 的時候,無論傳入 f1 的是右值仍是左值,由於 t 是一個變量名,傳入 f2 的時候都變成了左值,這就會形成由於調用 T 的拷貝構造函數而生成沒必要要的拷貝浪費大量資源。

那麼如今有一個叫 forward 的函數,就能夠這樣作:

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T&& t) {    //這是通用引用,而不是右值引用
    cout<"f1"<<endl;
    f2(std::forward<T>(t));  //std::forward<T>(t)用來把t轉發爲左值或右值,決定於T
}

這樣,f1 調用 f2 的時候,調用的就是移動構造函數而不是拷貝構造函數,能夠避免沒必要要的拷貝,這就叫「完美轉發」。

完美轉發,傻逼到家。

結語

本文開始提出的問題 auto &auto && 有什麼區別?這個問題就更復雜了,涉及到 Universal Reference 這個概念,能夠參考這 2 篇文章:

有空再說。

傻逼 C++ 。

相關文章
相關標籤/搜索