1. 程式人生 > 其它 >右值引用、移動語義

右值引用、移動語義

一、 什麼是左值、右值

首先不考慮引用以減少干擾,可以從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多用於模板,後續再補吧。。。