noexcept
一:C++ 98 中的異常描述符
在 C++ 98 中,描述一個函式是否發生異常是這樣的,
// in C++ 98 void func_not_throw() throw(); // 保證不會丟擲異常 void func_throw_int() throw(int); // 可能會丟擲一個型別為 int 的異常 void func_throw() throw(...); // 可能會丟擲某種型別的異常
但它有幾個弊端:
-
模板函式無法使用
template<class T> void simple_func(T k) { T x(k); x.do_something(); }
賦值函式、拷貝建構函式和
do_something()
都有可能丟擲異常,這取決於型別T
的實現。 -
顯示指明異常型別後的重構
void func() throw(k_too_small_exception) // 顯示指明異常型別 { int k = 3rd_lib_func(); // 一個第三方庫的介面 if (k < 0) throw k_too_small_exception(); }
起初這個第三庫的介面不會丟擲異常,但若隨著這個第三庫的更新,
3rd_lib_func()
加入了新的異常丟擲,那麼該函式也需要新加入異常型別。 -
異常丟擲後的棧展開(Stack Unwinding)
void func_not_throw() throw() // 保證不丟擲異常 { ... throw 1; // 但還是有異常丟擲 }
在 C++ 98 中,它會在呼叫處進行棧展開(Stack Unwinding)操作,但從函式的宣告來看,程式設計師可能並不會對它進行
try...catch...
異常處理,所以經過一幀幀的棧展開(Stack Unwinding)後,程式最終還是會終止,但不必要的棧展開(Stack Unwinding)還是做了。
其它更多的,可以參考https://stackoverflow.com/questions/88573/should-i-use-an-exception-specifier-in-c 和http://c.biancheng.net/cpp/biancheng/view/3027.html 。
最終你會發現,使用throw
異常說明符(Exception Specification),時常會感到它的雞肋、囉嗦和麻煩。因此 C++11 摒棄了throw
異常說明符(Exception Specification),並以一個新的說明符noexcept
代替。
二:C++ 11 noexcept
noexcept
緊跟在函式的引數列表後面,它只用來表明兩種狀態:“不拋異常”和“拋異常”。
void func_not_throw() noexcept; // 保證不丟擲異常 void func_not_throw() noexcept(true); // 和上式一個意思 void func_throw() noexcept(false); // 可能會丟擲異常 void func_throw(); // 和上式一個意思,若不顯示說明,預設是會丟擲異常(除了解構函式,詳見下面)
對於一個函式而言,noexcept
說明符要麼出現在該函式的所有宣告語句和定義語句,要麼一次也不出現。函式指標及該指標所指的函式必須具有一致的異常說明。在typedef
或類型別名中則不能出現noexcept
。在成員函式中,noexcept
說明符需要跟在const
及引用限定符之後,而在final
、override
或虛擬函式的=0
之前。
如果一個虛擬函式承諾了它不會丟擲異常,則後續派生的虛擬函式也必須做出同樣的承諾;與之相反,如果基類的虛擬函式允許丟擲異常,則派生類的虛擬函式既可以丟擲異常,也可以不允許丟擲異常。
需要注意的是,
編譯器不會檢查帶有noexcept說明符的函式是否有throw。
void func_not_throw() noexcept { throw 1; // 編譯通過,不會報錯(可能會有警告) }
這會發生什麼呢?程式會直接呼叫std::terminate
,並且不會棧展開(也可能會呼叫或部分呼叫,取決於編譯器的實現)。另外,即使你有使用try...catch...
,也無法捕獲這個異常。
#include <iostream> using namespace std; void func_not_throw() noexcept { throw 1; } int main() { try { func_not_throw(); // 直接 terminate,不會被 catch } catch (int) { cout << "catch int" << endl; } return 0; }
所以程式設計師在noexcept
的使用上要格外小心!
noexcept除了可以用作說明符(Specification),也可以用作運算子(Operator)。
noexcept
運算子是一個一元運算子,它的返回值是一個bool
型別的右值常量表達式,用於表示給定的表示式是否會丟擲異常。例如,
void f() noexcept { } void g() noexcept(noexcept(f)) // g() 是否是 noexcept 取決於 f() { f(); }
其中noexcept(f)
返回true
,則上式就相當於void g() noexcept(true);
。
解構函式預設都是noexcept的。
C++ 11 標準規定,類的解構函式都是noexcept
的,除非顯示指定為noexcept(false)
。
class A { public: A() {} ~A() {} // 預設不丟擲異常 }; class B { public: B() {} ~B() noexcept(false) {} // 可能會丟擲異常 };
在為某個異常進行棧展開(Stack Unwinding)的時候,會依次呼叫當前作用域下每個區域性物件的解構函式,如果這個時候解構函式又丟擲自己的未經處理的另一個異常,將會導致std::terminate
。所以解構函式應該從不丟擲異常。
三:顯示指定異常說明符的益處
-
語義
從語義上,
noexcept
對於程式設計師之間的交流是有利的,就像const
限定符一樣。 -
顯示指定
noexcept
的函式,編譯器會進行優化因為在呼叫
noexcept
函式時不需要記錄exception handler
,所以編譯器可以生成更高效的二進位制碼(編譯器是否優化不一定,但理論上noexcept
給了編譯器更多優化的機會)。另外編譯器在編譯一個noexcept(false)
的函式時可能會生成很多冗餘的程式碼,這些程式碼雖然只在出錯的時候執行,但還是會對 Instruction Cache 造成影響,進而影響程式整體的效能。 -
容器操作針對
std::move
的優化舉個例子,一個
std::vector<T>
,若要進行reserve
操作,一個可能的情況是,需要重新分配記憶體,並把之前原有的資料拷貝(Copy)過去,但如果T
的移動建構函式是noexcept
的,則可以移動(Move)過去,大大地提高了效率。#include <iostream> #include <vector> using namespace std; class A { public: A(int value) { } A(const A& other) { std::cout << "copy constructor\n"; } A(A&& other) noexcept { std::cout << "move constructor\n"; } }; int main() { std::vector<A> a; a.emplace_back(1); a.emplace_back(2); return 0; }
上述程式碼可能輸出:
move constructor
但如果把移動建構函式的
noexcept
說明符去掉,則會輸出:copy constructor
(你可能會問,為什麼在移動建構函式是
noexcept
時才能使用?這是因為它執行的是 Strong Exception Guarantee,發生異常時需要還原,也就是說,你呼叫它之前是什麼樣,丟擲異常後,你就得恢復成啥樣。但對於移動建構函式發生異常,是很難恢復回去的,如果在恢復移動(move)的時候發生異常了呢?但複製建構函式就不同了,它發生異常直接呼叫它的解構函式就行了。)
四:怎麼用,什麼時候用
-
解構函式
這不用多說,必須也應該為
noexcept
。 -
建構函式(普通、複製、移動),賦值運算子過載函式
儘量讓上面的函式都是
noexcept
。 -
還有那些你可以 100% 保證不會
throw
的函式不要太小瞧
noexcept
,不能保證的地方請不要用,否則會害人害己!切記。
(如果你還是不知道該在哪裡用,可以看下準標準庫Boost 的原始碼,全域性搜尋BOOST_NOEXCEPT
,你就懂了。)