Item 14: 如果函數不會拋出異常就把它們聲明爲noexcept

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

博客已經遷移到這裏啦

在C++98中,異常規範(exception specifications)是一個不穩定因素。你必須總結出一個函數可能會拋出的異常類型,所以如果函數的實現被修改了,異常規範可能也需要被修正。改變異常規範則又可能影響到客戶代碼,因爲調用者可能依賴於原先的異常規範。編譯器通常不會提供幫助來維護“函數實現,異常規範以及客戶代碼”之間的一致性。最終,大多數程序員覺得C++98的異常規範不值得去使用。

C++11中,對於函數的異常拋出行爲來說,出現了一種真正有意義的信息,它能說明函數是否有可能拋出異常。是或不是,一個函數可能拋出一個異常或它保證它不會拋出異常。這種“可能或絕不”二分的情況是C++11異常規範的基礎,這種異常規範從本質上替換了C++98的異常規範。(C++98風格的異常規範仍然是有效的,但是它們是被棄用了的。)在C++11中,無條件的noexcept就說明這個函數保證不會拋出異常。

在設計接口的時候,一個函數是不是應該這麼聲明(noexcept)是一個需要考慮的問題。函數的異常拋出行爲是客戶最感興趣的部分。調用者能詢問一個函數的noexcept狀態,並且這個詢問的結果能影響異常安全(exception safety)或着調用代碼的性能。因此,一個函數是否是noexcept和一個成員函數是否是cosnt,這兩個信息使同樣重要。當你知道一個函數不會拋出異常的時候卻不聲明它爲noexcept,就屬於一個不好的接口設計。

但是,這裏還有一個額外的動機讓我們把noexcept應用到不會產生異常的函數上:它允許編譯器產生更好的目標代碼。爲了理解爲什麼會這樣,讓我們檢查一下C++98和C++11中,對於一個函數不會拋出異常的不同解釋。考慮一個函數f,它保證調用者永遠不會收到異常。兩種不同的表示方法:

int f(int x) throw();           //C++98風格

int f(int x) noexcept;          //C++11風格

如果,運行時期,一個異常逃離了f,這違反了f的異常規範。在C++98的異常規範下,f的調用者的調用棧被解開了,然後經過一些不相關的動作,程序終止執行。在C++11的異常規範下,運行期行爲稍微有些不同:調用棧只有在程序終止前纔有可能被解開。

解開調用棧的時機,以及解開的可能性的不同,對於代碼的產生有很大的影響。在一個noexcept函數中,如果一個異常能傳到函數外面去,優化器不需要保持運行期棧爲解開的狀態,也不需要確保noexcept函數中的對象銷燬的順序和構造的順序相反(譯註:因爲noexcept已經假設了不會拋出異常,所以就算異常被拋出,大不了就是程序終止,而不可能處理異常)。使用“throw()”異常規範的函數,以及沒有異常規範的函數,沒有這樣的優化靈活性。三種情況能這樣總結:

RetType function(params) noexcept;          //優化最好

RetType function(params) throw();           //沒有優化

RetType function(params);                   //沒有優化

這種情況就能作爲一個充足的理由,讓你在知道函數不會拋出異常的時候,把它聲明爲noexcept。

對於一些函數,情況變得更加強烈(更多的優化)。move操作就是一個很好的例子。假設你有一份C++98代碼,它使用了std::vector。Widget通過一次次push_back來加到std::vector中:

std::vector<Widget> vw;

...

Widget w;

...                     //使用w

vw.push_back(w);        //把w加到vw中

...

假設這個代碼工作得很好,然後你也沒有興趣把它改成C++11的版本。但是,基於C++11的move語法能提升原來代碼的性能(當涉及move-enabled類型時)的事實,你想做一些優化,因此你要保證Widget有一個move operation,你要麼自己寫一個,要麼用函數生成器來實現(看Item 17)。

當一個新的元素被添加到std::vector時,可能std::vector剩下的空間不足了,也就是std::vector的size等於它的capacity(容量)。當發生這種事時,std::vector申請一個新的,更大的內存塊來保存它的元素,然後把原來的內存塊中的元素,轉移到新塊中去。在C++98中,轉移是通過拷貝來完成的,它先把舊內存塊中的所有元素拷貝到新內存塊中,再銷燬舊內存塊中的對象(譯註:再delete舊內存)。這種方法確保push_back能提供強異常安全的保證:如果一個異常在拷貝元素的時候被拋出,std::vector的狀態沒有改變,因爲在所有的元素都成功地被拷貝到新內存塊前,舊內存塊中的元素都不會被銷燬。

在C++11中,會進行一個很自然的優化:用move來替換std::vector元素的拷貝。不幸的是,這樣做會違反push_back的強異常安全保證。如果n個元素已經從舊內存塊中move出去了,在move第n+1個元素時,有一個異常拋出,push_back操作不能執行完。但是原來的std::vector已經被修改了:n個元素已經被move出去了。想要恢復到原來的狀態是不太可能的,因爲嘗試”把新內存塊中的元素move回舊內存塊中“的操作也可能產生異常。

這是一個嚴重的問題,因爲一些歷史遺留代碼的行爲可能依賴於push_back的強異常安全的保證。因此,除非知道它不會拋出異常,否則C++11中的push_back的實現不能默默地用move操作替換拷貝操作。在這種情況(不會拋出異常)下,用move替換拷貝操作是安全的,並且唯一的效果就是能提升代碼的性能。

std::vector::push_back採取”如果可以就move,不能就copy“的策略,並且在標準庫中,不只是這個函數這麼做。在C++98中,其他提供強異常安全的函數(比如,std::vector::reserve,std::deque::insert等等)也採取這樣的策略。如果知道move操作不會產生異常,所有這些函數都在C++11中使用move操作來替換原先C++98中的拷貝操作。但是一個函數怎麼才能知道move操作會不會產生異常呢?回答很明顯:它會檢查這個操作是否被聲明爲noexcept。

swap函數特別需要noexcept,swap是實現很多STL算法的關鍵部分,並且它也常常被拷貝賦值操作調用。它的廣泛使用使得noexcept提供的優化特別有價值。有趣的是,標準庫的swap是否是noexcept常常取決於用戶自定義的swap是否是noexcept。舉個例子,標準庫中,array和std::pair的swap這麼聲明:

template<class T, size_t N>
void swap(T (&a)[N],
          T (&a)[N])    noexcept(noexcept(swap(*a, *b)));

template<class T1, class T2>
sturct pair{
    ...
    void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && 
                                noexcept(swap(second, p.second)));
    ...
};

這些函數是條件noexcept(conditionally noexcept):它們是否是noexcept取決於noexcept中的表達式是否是noexcept。舉個例子,給出兩個Widget的數組,只有用數組中的元素來調用的swap是noexcept時(也就是用Widget來調用的swap是noexcept時),用數組調用的swap纔是noexcept。反過來,這也決定了Widget的二維數組是否是noexcept。相似地,std::pair

            你要記住的事
  • noexcept是函數接口的一部分,並且調用者可能會依賴這個接口。
  • 比起non-noexcept函數,noexcept函數可以更好地被優化。
  • noexcept對於move操作,swap,內存釋放函數和析構函數是特別有價值的,
  • 大部分函數是異常中立的而不是noexcept。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章