右值引用、移動語義
一、 什麼是左值、右值
首先不考慮引用以減少干擾,可以從2個角度判斷:
- 左值可以取地址、位於等號左邊;
- 而右值沒法取地址,位於等號右邊。
比如:
int num = 3; |
- num可以通過 & 取地址,位於等號左邊,所以
num
是左值。 - 3位於等號右邊,3沒法通過 & 取地址,所以3是個右值。
再者:
class Test { public: Test(int num = 0) { _num = num; } private: int _num; }; void main(){ Test test=Test(); } |
- 同樣的,test可以通過 & 取地址,位於等號左邊,所以test是左值。
- Test()是個臨時值,沒法通過 & 取地址,位於等號右邊,所以Test()是個右值。
因此,有地址的變數就是左值,沒有地址的字面值、臨時值就是右值。
二、什麼是左值引用、右值引用
引用本質是別名,可以通過引用修改變數的值,傳參時傳引用可以避免拷貝,其實現原理和指標類似。
1、左值引用
顧名思義,左值引用=對左值的引用。能指向左值,不能指向右值的
int num = 3; int &ref_num = num; // 左值引用指向左值,編譯通過 int &ref_num = 3; // 左值引用指向了右值,會編譯失敗 |
引用是變數的別名,由於右值沒有地址,沒法被修改,所以左值引用無法指向右值。
但是,const左值引用是可以指向右值的:
const int &ref_num = 3; // 編譯通過 |
const左值引用不會修改指向值,因此可以指向右值。
正是這個原因,函式引數的用法:void Test(const std::string& value);void Test(const std::vector<std::string>& vet)等等。
2、右值引用
同理,右值引用=對右值的引用。能指向右值,不能指向左值的。
int &&right_num = 3; // ok int num = 3; int &&left_num = num; // 編譯不過,右值引用不可以指向左值 right_num = 4; // 右值引用的用途:可以修改右值 |
三、右值引用能否指向左值
在C++11中,引入了新特性:std::move
int num = 3; // num是個左值 int &left_num = num; // 左值引用指向左值 int &&right_num = std::move(num); // 通過std::move將左值轉化為右值,可以被右值引用指向 cout << num; // 列印結果:3 |
本質:
- 其實std::move語義其本質並非將一個值移動到另一個值,釋放原記憶體。
- 而是把左值強制轉化為右值,讓右值引用可以指向左值。
- 其實現等同於一個型別轉換:
static_cast<T&&>(lvalue)
。
同理,右值引用能指向右值,本質上也是把右值提升為一個左值,並定義一個右值引用通過std::move指向該左值:
int &&ref_num = 3; ref_num = 4; 等同於以下程式碼: int temp = 30; int &&ref_num = std::move(temp); ref_num = 40; |
四、左值引用、右值引用本身是左值還是右值?
被宣告出來的左、右值引用都是左值。 因為被宣告出的左右值引用是有地址的,也位於等號左邊。
// 形參是個右值引用 void ChangeValue(int&& right_value) { right_value = 8; } int main() { int num = 3; // a是個左值 int &ref_a_left = num // ref_a_left是個左值引用 int &&ref_a_right = std::move(a); // ref_a_right是個右值引用 ChangeValue(num); // 編譯不過,num是左值,change引數要求右值 ChangeValue(ref_a_left); // 編譯不過,左值引用ref_a_left本身也是個左值 ChangeValue(ref_a_right); // 編譯不過,右值引用ref_a_right本身也是個左值 ChangeValue(std::move(a)); // 編譯通過 ChangeValue(std::move(ref_a_right)); // 編譯通過 ChangeValue(std::move(ref_a_left)); // 編譯通過 change(5); // 當然可以直接接右值,編譯通過 // 列印這三個左值的地址,都是一樣的 cout << &num << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; } |
最後,std::move會返回一個右值引用int &&
,它是左值還是右值呢?
從表示式int &&ref = std::move(num)
來看,右值引用ref
指向的必須是右值,所以move返回的int &&
是個右值。所以右值引用既可能是左值,又可能是右值嗎? 確實如此:右值引用既可以是左值也可以是右值,如果有名稱則為左值,否則是右值。
結論:
1、從效能上講,左右值引用沒有區別,傳參使用左右值引用都可以避免拷貝。
2、右值引用可以直接指向右值,也可以通過std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
3、作為函式形參時,右值引用更靈活。雖然const左值引用也可以做到左右值都接受,但它無法修改,有一定侷限性。
程式碼上的實踐
// std::vector和std::string的實際例子 int main() { std::string str1 = "aacasxs"; std::vector<std::string> vec; vec.push_back(str1); // 傳統方法,copy vec.push_back(std::move(str1)); // 呼叫移動語義的push_back方法,避免拷貝,str1會失去原有值,變成空字串 vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1會失去原有值 vec.emplace_back("axcsddcas"); // 當然可以直接接右值 } |
在vector和string這個場景,加個std::move
會呼叫到移動語義函式,避免了深拷貝。
可移動物件在<需要拷貝且被拷貝者之後不再被需要>的場景,建議使用std::move
觸發移動語義,提升效能。
比如:
moveable_objecta = moveable_objectb; 改為: moveable_objecta = std::move(moveable_objectb); |
還有些STL類是move-only
的,比如unique_ptr
,這種類只有移動建構函式,因此只能移動(轉移內部物件所有權,或者叫淺拷貝),不能拷貝(深拷貝):
std::unique_ptr<Test> ptr_test1 = std::make_unique<Test>(); // unique_ptr只有‘移動賦值過載函式‘,引數是&& ,只能接右值,因此必須用std::move轉換型別 std::unique_ptr<Test> ptr_test2 = std::move(ptr_test1);// 編譯不通過 std::unique_ptr<Test> ptr_test2 = ptr_test1; |
std::move本身只做型別轉換,對效能無影響。我們可以在自己的類中實現移動語義,避免深拷貝,充分利用右值引用和std::move的語言特性。
此外,還有完美轉發 std::forward,不過forward多用於模板,後續再補吧。。。