noexcept異常說明



noexcept異常使用
相比於斷言適用於排除邏輯上不可能存在的狀態,異常通常是用於邏輯上可能發生的錯誤。在C++98中,我們看到了一套完整的不同於C的異常處理系統。通過這套異常處理系統,C++擁有了遠比C強大的異常處理功能。
在異常處理的代碼中,程序員有可能看到過如下的異常聲明表達形式:
void excpt_func() throw(int, double) { ... }
在excpt_func函數聲明之后,我們定義了一個 動態異常聲明throw(int, double), 該聲明指出了excpt_func可能拋出的異常的類型。事實上,該特性很少被使用,因此在C++11中被棄用了(參見附錄B),而表示函數不會拋出異常的動態異常聲明throw()也被新的noexcept異常聲明所取代。
noexcept形如其名地,表示其修飾的函數不會拋出異常。 不過與throw()動態異常聲明不同的是,在C++11中如果noexcept修飾的函數拋出了異常,編譯器可以選擇直接調用std::terminate()函數來終止程序的運行,這比基於異常機制的throw()在效率上會高一些。 這是因為異常機制會帶來一些額外開銷,比如函數拋出異常,會導致函數棧被依次地展開(unwind),並依幀調用在本幀中已構造的自動變量的析構函數等。
從語法上講,noexcept修飾符有兩種形式,一種就是簡單地在函數聲明后加上noexcept關鍵字。比如:
void excpt_func() noexcept;
另外一種則可以接受一個常量表達式作為參數,如下所示:
void excpt_func() noexcept (常量表達式);
常量表達式的結果會被轉換成一個bool類型的值。該值為true,表示函數不會拋出異常,反之,則有可能拋出異常。這里, 不帶常量表達式的noexcept相當於聲明了noexcept(true),即不會拋出異常。
在通常情況下,在C++11中使用noexcept可以有效地阻止異常的傳播與擴散。我們可以看看下面這個例子,如代碼清單2-12所示。
1 #include <iostream>
2 using namespace std; 3 void Throw() { throw 1; } 4 void NoBlockThrow() { Throw(); } 5 void BlockThrow() noexcept { Throw(); } 6
7 int main() { 8 try { 9 Throw(); 10 } 11 catch(...) { 12 cout << "Found throw." << endl; // Found throw.
13 } 14
15 try { 16 NoBlockThrow(); 17 } 18 catch(...) { 19 cout << "Throw is not blocked." << endl; // Throw is not blocked.
20 } 21
22 try { 23 BlockThrow(); // terminate called after throwing an instance of 'int'
24 } 25 catch(...) { 26 cout << "Found throw 1." << endl; 27 } 28 }
在上述程序中,我們定義了Throw函數,該函數的唯一作用是拋出一個異常。而NoBlockThrow是一個調用Throw的普通函數,BlockThrow則是一個noexcept修飾的函數。從main的運行中我們可以看到,NoBlockThrow會讓Throw函數拋出的異常繼續拋出,直到main中的catch語句將其捕捉。而BlockThrow則會直接調用std::terminate中斷程序的執行,從而阻止了異常的繼續傳播。從使用效果上看,這與C++98中的throw()是一樣的。
而noexcept作為一個操作符時,通常可以用於模板。比如:
1 template <class T>
2 void fun() noexcept(noexcept(T())) {}
這里, fun函數是否是一個noexcept的函數,將由T()表達式是否會拋出異常所決定。這里的第二個noexcept就是一個noexcept操作符。當其參數是一個有可能拋出異常的表達式的時候,其返回值為false,反之為true(實際noexcept參數返回false還包括一些情況,這里就不展開講了)。這樣一來,我們就可以使模板函數根據條件實現noexcept修飾的版本或無noexcept修飾的版本。從泛型編程的角度看來,這樣的設計保證了關於“函數是否拋出異常”這樣的問題可以通過表達式進行推導。因此這也可以視作C++11為了更好地支持泛型編程而引入的特性。
雖然noexcept修飾的函數通過std::terminate的調用來結束程序的執行的方式可能會帶來很多問題,比如無法保證對象的析構函數的正常調用,無法保證棧的自動釋放等,但很多時候,“暴力”地終止整個程序確實是很簡單有效的做法。 事實上,noexcept被廣泛地、系統地應用在C++11的標准庫中,用於提高標准庫的性能,以及滿足一些阻止異常擴散的需求。
比如在C++98中,存在着使用throw()來聲明不拋出異常的函數
1 template<class T> class A { 2 public: 3 static constexpr T min() throw() { return T(); } 4 static constexpr T max() throw() { return T(); } 5 static constexpr T lowest() throw() { return T(); } 6 ...
而在C++11中,則使用noexcept來替換throw()。
1 template<class T> class A { 2 public: 3 static constexpr T min() noexcept { return T(); } 4 static constexpr T max() noexcept { return T(); } 5 static constexpr T lowest() noexcept { return T(); } 6 ...
1 void* operator new(std::size_t) throw(std::bad_alloc); 2 void* operator new[](std::size_t) throw(std::bad_alloc);
而在C++11中,則使用noexcept(false)來進行替代。
1 void* operator new(std::size_t) noexcept(false); 2 void* operator new[](std::size_t) noexcept(false);
當然,noexcept更大的作用是保證應用程序的安全。比如 一個類析構函數不應該拋出異常,那么對於常被析構函數調用的delete函數來說,C++11默認將delete函數設置成noexcept,就可以提高應用程序的安全性。
1 void operator delete(void*) noexcept; 2 void operator delete[](void*) noexcept;
而同樣出於安全考慮,C++11標准中讓類的析構函數默認也是noexcept(true)的。當然,如果程序員顯式地為析構函數指定了noexcept,或者類的基類或成員有noexcept(false)的析構函數,析構函數就不會再保持默認值。我們可以看看下面的例子:
1 #include <iostream>
2 using namespace std; 3
4 struct A { 5 ~A() { throw 1; } 6 }; 7
8 struct B { 9 ~B() noexcept(false) { throw 2; } 10 }; 11
12 struct C { 13 B b; 14 }; 15
16 int funA() { A a; } 17 int funB() { B b; } 18 int funC() { C c; } 19
20 int main() { 21 try { 22 funB(); 23 } 24 catch(...){ 25 cout << "caught funB." << endl; // caught funB.
26 } 27
28 try { 29 funC(); 30 } 31 catch(...){ 32 cout << "caught funC." << endl; // caught funC.
33 } 34
35 try { 36 funA(); // terminate called after throwing an instance of 'int'
37 } 38 catch(...){ 39 cout << "caught funA." << endl; 40 } 41 }
在代碼中,無論是析構函數聲明為noexcept(false)的類B,還是包含了B類型成員的類C,其析構函數都是可以拋出異常的。只有什么都沒有聲明的類A,其析構函數被默認為noexcept(true),從而阻止了異常的擴散。這在實際的使用中,應該引起程序員的注意。
