對於c++11來說移動語義是一個重要的概念,一直以來我對這個概念都似懂非懂。最近翻翻資料感覺突然開竅,因此記下。其實搞懂之後就會發現這個概念很簡單,並無什麼高深的地方。 先說說右值引用。右值一般指的是表示式中的臨時變數,在c++中臨時變數在表達式結束後就被銷毀了,之後程式就無法再引用這個變數了。但是 ...
對於c++11來說移動語義是一個重要的概念,一直以來我對這個概念都似懂非懂。最近翻翻資料感覺突然開竅,因此記下。其實搞懂之後就會發現這個概念很簡單,並無什麼高深的地方。
先說說右值引用。右值一般指的是表示式中的臨時變數,在c++中臨時變數在表達式結束後就被銷毀了,之後程式就無法再引用這個變數了。但是c++11提供了一個方法,讓我們可以引用這個臨時變數。這個方法就是所謂的右值引用。
那麼右值引用有什麼用呢?避免記憶體copy!
不同於其它語言,在c++里變數是值語義(在JAVA、Python變數是引用語義)。因此對於賦值操作意味著記憶體拷貝而不是簡單的賦值指針。而右值引用的一個作用就是我們可以通過重新利用臨時變數(右值)來避免無意義的記憶體copy。
看這個例子(取自Rvalue References: C++0x Features in VC10, Part 2)
1 string s0(“my mother told me that”); 2 string s1(“cute”); 3 string s2(“fluffy”); 4 string s3(“kittens”); 5 string s4(“are an essential part of a healthy diet”); 6 7 string dest = s0 + ” ” + s1 + ” ” + s2 + ” ” + s3 + ” ” + s4;
第7行對string求和中,每次調用operator+()都會創建一個臨時變數,一共創建了8個臨時的string對象。而這裡真正低效的原因是,每次創建一個string對象都需要從堆上申請空間,將字元串的內容copy進來,並且這些臨時的string都是只用一次。
顯然這是低效的,為什麼不這樣子做呢:假設s0+""的時候創建的臨時變數是temp_str,此後我們一直是用這個臨時變數而不是重寫創建。這樣做表達式的結果是不會有任何改變,並且因為在opertor+()創建的都是程式其它地方不會引用到的臨時變數,所以這樣做也不會有任何副作用。相當於我們偷偷的做了個其它人無法察覺的小優化。
要如何做才能引用這個臨時的string對象呢?答案就是右值引用。通過右值引用我們就能重新利用表達式中的臨時變數,並且以更高效的指針swap來避免記憶體copy。
需要說明的一點是,右值引用優化的是避免對象在堆空間的記憶體的copy。在堆上的記憶體我們可以簡單的通過指針交換來傳遞記憶體資源的所有權(類似於vector的swap方法),而對於棧上的記憶體不可避免還是需要copy。舉一個例子這裡移動對象有點像放風箏:我放風箏,放著放著覺得累了就交給你,具體是怎麼交法呢?你先製作一個和我手上拿的一模一樣的手柄,接著我再把線剪短遞給你,你把線綁在你的手柄上,交接完畢!手柄對應是棧空間、風箏對應是堆空間。
這種技術十分有用,不僅僅是在處理臨時變數的時候起作用,有的時候我們想要使用這個轉移資源(記憶體)的效果時,也可以強制將類型轉為右值引用(std::move)來觸發對象移動。
舉一個例子,比如說對於vector的動態擴容。熟悉vector的實現的都知道,在對一個vector進行push_back時有可能會觸發記憶體的重新分配,這個時候需要把原來記憶體的對象copy到新分配的記憶體上,最後再釋放原來的記憶體。假設這個vector裡面存放的是string對象,那麼我們在執行簡單的對象賦值(調用的是string::operator=()方法)的過程中,我們copy的不僅僅是sizeof(string)的記憶體,我們還copy這個string內部指針指向堆空間上的記憶體。通過觀察可以發現,其實我們完全不必去拷貝內部指針指的那部分記憶體,因為原來的string對象在賦值完後就要被銷毀,如果我們將這個指針偷偷的拿過來(swap),程式的其它部分不會有任何察覺。為了實現這樣的操作我們需要做以下兩件事情:
- 對string實現一個移動構造函數、移動賦值函數。這些函數對內部指針進行swap操作,而不是copy操作。
- 通過std::move來強化轉化成右值引用,用以觸發移動賦值函數。編譯器正是通過參數類型是T&&,才知道應該使用移動版本的operator=()而不是copy版本的operator=()。
1 new_stri = std::move(old_str);
要對old_str轉化為右值引用是因為它並不是真正的右值,它不是一個臨時變數。但因為它即將被銷毀,所以效果等同於一個臨時變數。因此可以安全的轉換,從而調用移動賦值函數並悄悄的"移動"它的記憶體資源。
推薦閱讀:
參考資料: