1. 程式人生 > 實用技巧 >C++右值引用

C++右值引用

右值和左值

說到右值,先看一下什麼是右值,在C++中,一個值要麼是右值,要麼是左值。

  • 左值:是指表示式結束後依然存在的持久化物件

  • 右值:是指表示式結束時就不再存在的臨時物件

所有的具名變數或者物件都是左值,而右值不具名。

例如,常見的右值“abc",123等都是右值。

右值引用用以引用一個右值,可以延長右值的生命期,比如:

int&& i = 123;          //正確
int&& j = std::move(i); //正確
int&& k = i;            //錯誤,i是一個左值,右值引用只能引用右值

可以通過下面的程式碼,更深入的體會左值引用和右值引用的區別:

int i;
int&& j = i++;
int&& k = ++i; //錯誤:Rvalue reference to type 'int' cannot bind to lvalue of type 'int'
int& m = i++;  //錯誤:Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
int& l = ++i;

為什麼需要右值引用

C++引入右值引用之後,可以通過右值引用,充分使用臨時變數,減少不必要的拷貝,提高效率。如下程式碼,均會產生臨時變數:

class RValue 
{
	//...
};

RValue get() {
    return RValue(); //臨時變數
}

為了充分利用右值的資源,減少不必要的拷貝,C++11引入了右值引用(&&),移動建構函式,移動複製運算子以及std::move。

將上面的類定義補充完整:

#include <iostream>
#include <string>
using namespace std;

struct RValue {
    RValue():sources("hello C++"){}
    RValue(RValue&& a)
    {
        sources = std::move(a.sources);
        cout<<"&& RValue"<<endl;
    }

    RValue(const RValue& a)
    {
        sources = a.sources;
        cout<<"& RValue"<<endl;
    }

    void operator=(const RValue&& a)
    {
        sources = std::move(a.sources);
        cout<<"&& ="<<endl;
    }

    void operator=(const RValue& a)
    {
        sources = a.sources;
        cout<<"& ="<<endl;
    }

    string sources;
};

RValue get()
{
    RValue a;
    return a;
}

void put(RValue){}

int main() {
    RValue a = get();
    return 0;
}

不過,當執行的時候卻發現沒有任何輸出?

這是因為,編譯器做了優化,編譯的時候加上-fno-elide-constructors,去掉優化。再次執行輸出如下:

&& RValue
&& RValue

通過上面的程式碼,可以看出,在沒有加-fno-elide-constructors選項時,編譯器做了優化,沒有臨時變數的生成。在加了-fno-elide-constructors選項時,get函式產生了兩次臨時變數。

將get函式稍微修改一下:

RValue get()
{
    RValue a;
    return std::move(a);
}

首先先不加-fno-elide-constructors選項,執行如下:

&& RValue

加上-fno-elide-constructors選項,執行結果:

&& RValue
&& RValue

只是簡單的修改了一下,std::move(a),在編譯器做了優化的情況下,用了std::move,反而多做了一次拷貝。

其實,RValue如果在沒有定義移動建構函式,重複上面的操作,生成臨時變數的次數還是一樣的,只不過,呼叫的是拷貝構造函數了而已。

通過get函式可以知道,亂用std::move在編譯器開啟建構函式優化的場景下反而增加了不必要的拷貝。那麼,std::move應該在什麼場景下使用?

std::move詳解

移動建構函式的原理

通過移動構造,b指向a的資源,a不再擁有資源,這裡的資源,可以是動態申請的記憶體,網路連結,開啟的檔案,也可以是本例中的string。這時候訪問a的行為時未定義的,比如,如果資源是動態記憶體,a被移動之後,再次訪問a的資源,根據移動建構函式的定義,可能是空指標,如果是資源上文的string,移動之後,a的資源為空字串(string被移動之後,為空字串)。

可以通過下面程式碼驗證,修改main函式:

int main() {
    RValue a, b;
    RValue a1 = std::move(a);
    cout << "a.sources:" << a.sources << endl;
    cout << "a1.sources:" << a1.sources << endl;
    RValue b1(b);
    cout << "b.sources:" << b.sources << endl;
    cout << "b1.sources:" << b1.sources << endl;
    return 0;
}

執行結果如下:

&& RValue
a.sources:
a1.sources:hello C++
& RValue
b.sources:hello C++
b1.sources:hello C++

通過移動建構函式之後,a的資源為空,b指向了a的資源。通過拷貝建構函式,b複製了a的資源。

std::move的原理

std::move的定義:

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

這裡,T&&是通用引用,需要注意和右值引用(比如int&&)區分。通過move定義可以看出,move並沒有”移動“什麼內容,只是將傳入的值轉換為右值,此外沒有其他動作。std::move+移動建構函式或者移動賦值運算子,才能充分起到減少不必要拷貝的意義。

std::move使用場景

在之前的專案中看到有的同事到處使用std::move,好像覺得使用了std::move就能移動資源,提升效能一樣,在我看來,std::move主要使用在以下場景:

使用前提如下:

  1. 定義的類使用了資源並定義了移動建構函式和移動賦值運算子

  2. 該變數即將不再使用

RValue a, b;
	
//對a,b做一系列操作之後,不再使用a,b,但需要儲存到智慧指標或者容器之中
unique_ptr<RValue> up(new RValue(std::move(a)));
vector<RValue*> vr;
vr.push_back(new RValue(std::move(b)));

//臨時容器中儲存的大量的元素需要複製到目標容器之中	
vector<RValue> vrs_temp;
vrs_temp.push_back(RValue());
vrs_temp.push_back(RValue());
vrs_temp.push_back(RValue());
vector<RValue> vrs(std::move(vrs_temp));

在沒有右值引用之前,為了使用臨時變數,通常定義const的左值引用,比如const string&,在有了右值引用之後,為了使用右值語義,不要把引數定義為常量左值引用。否則,傳遞右值時呼叫的是拷貝建構函式。

void put(const RValue& c){
    cout<<"----------"<<endl;
	unique_ptr<RValue> up(new RValue(std::move(c)));
    cout<<"----------"<<endl;
}

int main() {
    RValue c;
    put(std::move(c));
    return 0;
}

執行結果:

----------
& RValue
----------

不使用左值常量引用:

void put(RValue c)
{
    cout<<"----------"<<endl;
    unique_ptr<RValue> up(new RValue(std::move(c)));
    cout<<"----------"<<endl;
}

int main() {
    RValue c;
    put(std::move(c));
    return 0;
}

執行結果

&& RValue
----------
&& RValue
----------

這是因為,根據通用引用的定義,std::move(c)過程中,模板引數被推倒為const RValue&,因此,呼叫拷貝建構函式。