對於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轉化為右值引用是因為它並不是真正的右值,它不是一個臨時變量。但因為它即將被銷毀,所以效果等同於一個臨時變量。因此可以安全的轉換,從而調用移動賦值函數並悄悄的"移動"它的內存資源。
推薦閱讀:
參考資料: