右值系列之四 再論賦值

2021-06-02 04:36:49 字數 2702 閱讀 8636

view plain

mutex m1, m2;  

std::vector> v1, v2;  

v1.push_back(shared_ptr(new

lock(m1)));  

v2.push_back(shared_ptr(new

lock(m2)));  

v2 = v1;              // #1

…  v2 = std::move(v1);   // #2 - done with v1 now, so i can move it

…  

賦值#1釋放了 v2 所獨佔的所有鎖(並讓 v2 成為 v1 所擁有的所有東西的共用者)。但是賦值#2並沒有即時效果,除了交換各自的鎖擁有權。在#2的情況下,原來由 v2 所持有的鎖不會釋放,直至 v1 離開其範圍域,這可能是很久很久以後。上鎖與解鎖的順序與否是正確的多執行緒程式和死鎖之間的區別,所以這是乙個嚴重的問題,我們前面的關於轉移賦值的描述需要加以修正:

轉移賦值的語義:轉移賦值操作符從「偷取」它的引數的值,將該引數置於可析構和可賦值的狀態,並保留左運算元的任何使用者可見的***。

有了以上指引的幫助,現在我們可以修改我們對 std::vector 的轉移賦值實現:

view plain

vector& operator=(vector&& rhs)    

在實踐中,最先的 clear() 通常沒有什麼作用(也沒有什麼開銷),因為轉移賦值的目標通常已經是空的了,通常它本身就是前面某次轉移賦值的源物件。在大多數標準演算法以及前面我們作為了乙個例子的插入排序演算法中,這確實是真的。不過,加上這乙個 clear() 可以在向乙個非空的左運算元進行轉移賦值時避免麻煩。

正如前一篇所提到的,複製賦值有一種「規範性的實現」,基於一次複製構造和一次交換:

view plain

t& operator=(t rhs)   

它有很多好處:

只要你實現了轉移構造和廉價、無丟擲的交換,以上也可以看作是乙個好的轉移賦值操作符的實現。右值引數可以轉移構造至 x 然後交換給 *this。華而不實!如果你已經使用了規範的複製賦值操作符,畢竟你可能就不需要再寫乙個轉移賦值操作符了。

這就是說,即使是在c++03中,這種「規範實現」往往過早地從左值進行複製。對於 std::vector 的情形,在賦值號左邊的物件也許有足夠的空間,你只需要銷毀其中的元素然後將右邊的元素複製過來就可以了,這樣可以避免一次昂貴的記憶體分配,而且如果源 vecotr 非常大的話,可能會導致記憶體不足。

所以,std::vector 使用了一種更為經濟的賦值操作符,其簽名為 vector& operator=(vector const&),該實現允許將左值的複製延遲至已確認了必須要複製之後。不幸的是,如果我們試圖將這個規範的複製賦值簽名用於右值的話,將產生歧義的過載。相反,我們需要類似的東西,即效果相同但是將臨時物件的生成移至賦值操作符之內:

view plain

vector& operator=(vector&& rhs)    

看上去,這確實就是轉移賦值語義的泛型實現,不過這裡有另外乙個問題。我們來計算一下這個操作的總開銷:

現在來和「清除後交換」的實現比較一下:

view plain

vector& operator=(vector&& rhs)    

回想一下早前我們提到過的,多數(甚至可能是絕大多數)轉移賦值的左運算元是乙個剛剛被轉移走的物件。在一般情形下,析構 *this 原用內容——通過 clear 或是通過臨時物件的析構——的開銷只是單次的測試和跳轉。所以,其它的操作是主要開銷,而「清除後交換」的實現要比另乙個實現差不多快上兩倍。

實際上,我們可能還可以做得更好。是的,理論上 swap 操作可以讓我們**利用左運算元的空間而不是過早地把它處理掉,但實際問題是該什麼時候做呢?答案是僅當從乙個已被 std::move 的左值進行賦值的時候,因為真正的右值很快會被銷毀。那麼有多大機會左運算元真的有足夠空間呢?機會不大,因為左運算元通常都是乙個剛剛被轉移走的物件。因此,以下這樣的實現有可能是真正最高效的:

view plain

vector& operator=(vector&& rhs)  

this

->begin_ = rhs.begin_;  

this

->end_ = rhs.end_;  

this

->cap_ = rhs.cap_;  

rhs.begin_ = rhs.end_ = rhs.cap_ = 0;  

return

*this

;  }  

如果你想知道以上各種實現的實際效果, 我用了這個測試檔案來證明不僅可以通過實現轉移語義來提高速度,還可以進一步地優化它。這個測試是對乙個 std::vector 和乙個 boost::array 使用支援轉移語義的 std::rotate 演算法。如你所見,對於 std::vector,「規範的」轉移語義實現要比「清除後交換」實現好一點點,不過,通過以最少操作實現轉移賦值,還可以更快。

vector 轉移賦值的實現比較

array 轉移賦值的實現比較

對於 boost::array(我們假設其結構類似於 boost::tuple),輪到使用 swap 的實現比最簡單的乙個乙個元素的轉移實現差不多慢上三倍,而經過仔細優化,我們還可以做得更好。

因此,這個故事的寓意是:要警惕公式化的轉移操作實現;記住,轉移語義就是為了優化,所以轉移操作必須是非常快的,這裡一點開銷,那裡一點開銷,就有可能產生明顯的差異。

右值系列之六 向前,向前!

除了提供轉移語義,右值引用的另乙個主要用途是解決 完美 在這裡,的指將乙個泛型函式的實參 至另乙個函式而不會拒絕掉第二個引數可接受的任何引數,也不會丟失關於這些引數的cv限定或左右值屬性的任何資訊,而且還無須採用過載。在c 03中,最佳的近似是將所有右值變為左值,並且需要兩個過載。考慮以下例子 pr...

跟老齊學Python之編寫類之四再論繼承

複製 如下 usr bin env python coding utf 8 class person def init self,name,email self.name name self.email email class programmer person def init self,name...

零基礎學python 編寫類之四再論繼承

usr bin env python coding utf 8 class person def init self,name,email self.name name self.email email class programmer person def init self,name,email...