什麼是noexcept
在CppCon 2018上,Jason Turner在talk《Applied Best Practices》中提出總結了幾點best practices,我會通過幾篇文章做一些筆記,本文章對c++11中noexcept進行一些總結。
noexcept specifier
noexcept specifier: Specifies whether a function could throw exceptions cppreference.com.
noexcept specifier用於指明一個函數是否有可能拋出異常。noexcept specifier如下幾種形式:
- noexcept
- noexcept(expression)
- throw()
其中noexcept等同於noexcept(true),用於指明函數不會拋出異常。而*noexcept(false)*意味着函數可能會拋出異常。第三種形式不需要浪費精力去深究,因爲它將在c++20中移除。
C++中的每個函數要麼是non-throwing,要麼是potentially throwing,判斷一個函數是否拋出異常的詳細的標準見noexcept specifier.
noexcept operator
noexcept operator是一個用於判斷(編譯期)指定的表達式是否會拋出異常運算符,它可以和noexcept specifier一起使用通過某些已有信息來指定某個函數是否會拋出異常。
另外noexcept operator的操作數是unevaluated operands,類似於的還有
- sizeof operator
- typeid operator
- require expression
- decltype specifier
noexcept operator需要編譯器的支持,編譯器需要按照一定的標準來判斷指定的表達式是否會拋出異常,標準如下:
The result is false if the expression contains at least one of the following potentially evaluated constructs:
- call to any type of function that does not have non-throwing exception specification, unless it is a constant expression
- throw expression
- dynamic_cast expression when the target type is a reference type, and conversion needs a run time check
- typeid expression when argument type is polymorphic class type
In all other cases the result if true. until c++17
注意這些推斷都是簡單的編譯期的分析,沒有使用flow-analysis的分析,如下示例代碼,最終的結果也是false。
int foo() { return 1; }
int goo() noexcept{ return 0; }
int main() {
std::cout << noexcept(0 > 1 ? foo() : goo()) << '\n';
}
noexcept緣起
noexcept緣起於C++0x中的move constructor
,在由David Abrahams和Douglas Gregor寫的文章《Rvalue References and Exception Safety》中介紹了move constructor在exception發生時只支持basic guarantee,
To resolve this dilemma, the C++ standard library provides a set of exception-safety guarantees that share the burden of producing correct programs between implementers of the standard library and users of the standard library:
[3a] ‘‘Basic guarantee for all operations:’’ The basic invariants of the standard library are maintained, and no resources, such as memory, are leaked.
[3b] ‘‘Strong guarantee for key operations:’’ In addition to providing the basic guarantee, either the operation succeeds, or has no effects. This guarantee is provided for key library operations, such as push_back(), single-element insert() on a list, and uninitialized_copy()
(§E.3.1, §E.4.1).
[3c] ‘‘Nothrow guarantee for some operations:’’ In addition to providing the basic guarantee, some operations are guaranteed not to throw an exception This guarantee is provided for a few simple operations, such as swap() and pop_back() (§E.4.1). Standard-Library Exception Safety
注:Douglas Gregor是boost的早期重要成員,以及clang和swift的主要作者
《Rvalue References and Exception Safety》以vector中的push_back爲例,描述了move constructor在push_back出現異常時,可能導致的問題。
T* reallocate(T *old_ptr, size_t old_capacity) {
// #1: allocate new storage
T* new_ptr = (T*)new char[sizeof(T) * old_capacity * 2];
// #2: try to move the elements to the new storage
unsigned i = 0;
try {
// #2a: construct each element in the new storage from the correspoding
// element in the old storage, treating the old elements as rvalues.
for (; i < old_capacity; ++i)
new (new_ptr + i) T(std::move(old_ptr[i])); // "move" operation
} catch(...) {
// #2b: destory the copies and deallocate the new storage
for (unsigned v = 0; v < i; ++v)
new_ptr[v]->~T();
delete[]((char*)new_ptr);
throw;
}
// #3: free the old storage
for (i = 0; i < old_capacity; ++i)
old_capacity[i]->~T();
delete[] ((char*)old_ptr);
return new_ptr;
}
注:上述代碼來自於《Rvalue References and Exception Safety》
在push_back的時候存在如下兩種情況,
- size < capacity,不需要重新分配
- size == capacity,需要重新分配一塊更大的內存,然後將原有的元素一一拷貝或移動到新的內存空間中
上述代碼示例描述的就是重新分配更大塊內存的情況,此時如果元素有move constructor,那麼就會調用move construtor,但如果在調用某個move constructor時,出現了異常,那麼此時原有內存空間中已經移動過的對象已經處於moved from state,而且這幾乎是不可逆的,因爲當你嘗試將對應元素從新內存空間移動回原有內存空間時,move constructor有可能還會出現異常。所以此時,當元素的move constructor可能會出現異常時,push_back只能作出basic guarantee***,例如vector的capacity,size等保持不變,也沒有內存泄漏,但此時某些元素對象的狀態已經發生改變。整個過程如下圖所示:
*注:關於moved-from state*,請參見EXP63-CPP. Do not rely on the value of a moved-from object*
所以《Rvalue References and Exception Safety》提出瞭如下用於解決該問題的方式:
- 使用concept,例如require NothrowMoveConstructible<T>,如果不滿足則退化到copy constructor
- 提出了noexcept限定符,編譯器會靜態檢查是否滿足noexcept屬性,如果被noexcept限定的函數會拋出異常,則這個程序是ill-formed的
- move constructors和destructors默認noexcept。如果用戶需要move constructors和destructors拋出異常,則需要顯示地使用throw表示,STL不會採用這些會拋出異常的move constructor和destructor。
《Rvalue References and Exception Safety》的主要貢獻如下:
- 發現了move constructor破壞了STL中strong exception guarantee這個問題
- noexcept限定符來說明某個函數不會拋出異常,並靜態檢查該函數是否違反了noexcept這一性質
noexcept的改進
《Rvalue References and Exception Safety》存在如下幾個問題:
- 對於每個noexcept限定符,編譯器都要進行靜態檢查,開銷是一個問題
- 靜態檢查(使用比較簡單的程序分析)過於嚴格,可能會有“誤報”,也就是該函數不可能拋異常,但是編譯器在分析的時候卻認定它可能會拋異常
- move constructor默認都是noexcept,只是爲了在類似於STL場景中保證strong guarantee,但是在某些用戶的場景中,basic guarantee也是可以接受的(畢竟move constructor帶來的性能提升太吸引人了),所以最好可以在使用的地方進行控制,是否選擇採用可能會拋出異常的move constructor
基於此《Allowing Move Constructors to Throw》提出如下改進:
- 提供了std::move_if_noexcept
- 提供了operator noexcept,允許用戶按需檢查(編譯期靜態檢查)某個函數是否會拋出異常。從而避免對所有使用noexcept限定符的函數進行靜態檢查。
另外在《Allowing Move Constructors to Throw (Rev. 1)》中提出了重要的改進就是如果使用noexcept限定的函數拋出了異常,則調用std::terminate(一般由std::abort實現),並且保證這個異常不會跳出該函數。
到此C++11中的noexcept已經初見雛形,noexcept的提出和改進過程可以總結爲下面的描述:
If the noexcept feature appears to you incomplete, prepared in a rush, or in need of improvement, note that all C++ Committee members agree with you. The situation they faced was that a safety problem with throwing move operations was discovered in the last minute and it required a fast solution. The current solution does solve the problem neatly: there will be no silently generated throwing moves that will make STL containers silently break contracts. 《Using noexcept》
注:noexcept的由來和改進可以參照Using noexcept
The overhead of exception handling
留坑
Zero-overhead deterministic exceptions: Throwing values
When should I really use noexcept?
留坑
就像是否有必要在所有有返回值的函數前面加上*[[nondiscard]]*一樣,nonexcept同樣存在這個問題,那麼何時需要爲函數加上noexcept限定符號呢?關於這個問題衆說紛紜,還沒有確切的答案,例如在什麼情況下做什麼。相關資料如下: