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主要使用在以下場景:
使用前提如下:
-
定義的類使用了資源並定義了移動建構函式和移動賦值運算子
-
該變數即將不再使用
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&,因此,呼叫拷貝建構函式。