C++異常處理

異常處理（exception handling）是C++的一項語言機制，用於在程序能處理異常事件。

異常事件在C++中表示為異常對象（exception object）。異常事件發生時，由作業系統為程序設置當前異常對象，然後執行程序的當前異常處理代碼塊，在包含了異常出現點的最內層的`try`塊，依次匹配同級的`catch`語句。如果匹配`catch`語句成功，則在該catch塊內處理異常；然後執行當前`try...catch...`塊之後的代碼。如果在當前的`try...catch...`塊沒有能匹配該異常對象的`catch`語句，則由更外一層的`try...catch...`塊處理該異常；如果當前函數內的所有`try...catch...`塊都不能匹配該異常，則遞歸回退到調用棧的上一層函數去處理該異常。如果一直回退到主函數`main()`都不能處理該異常，則調用系統函數`terminate()`終止程序。

## throw

`throw`是一個C++關鍵字，與其後的操作數構成了`throw`語句，語法上類似於`return`語句。`throw`語句必須被包含在`try`塊之中；可以是被包含在調用棧的外層函數的`try`中。

執行`throw`語句時，其操作數的結果作為對象被複製構造為一個新的對象，放在記憶體的特殊位置（既不是堆也不是棧，Windows上是放在「線程信息塊TIB」中）。這個新的對象由本級的`try`所對應的`catch`語句逐個做類型匹配；如果匹配不成功，則與本函數的外層`catch`語句依次做類型匹配；如果在本函數內不能與`catch`語句匹配成功，則遞歸回退到調用棧的上一層函數內從函數調用點開始繼續與`catch`語句匹配。重複這一過程直到與某個`catch`語句匹配成功或者直到主函數main都不能處理該異常。

因此，`throw`語句拋出的異常對象不同於一般的局部對象。一般的局部對象會在其作用域結束時被析構。而`throw`語句拋出的異常對象駐留在所有可能被激活的`catch`語句都能訪問到的記憶體空間中。

`throw`語句拋出的異常對象在匹配成功的`catch`語句的結束處被析構（即使該catch語句使用的是非「引用」的傳值參數類型）。

由於`throw`語句都進行了一次副本拷貝，因此異常對象應該是可以copy構造的。但對於Microsoft Visual C++編譯器，異常對象的複製構造函數即使私有的情形，異常對象仍然可以被`throw`語句正常拋出；但在catch語句的參數是傳值時，在catch語句處編譯報錯：「cannot be caught as the destructor and/or copy constructor are inaccessible」。

拋出一個表達式時，被拋出對象的靜態編譯時類型將決定異常對象的類型。

## catch

`catch`語句匹配被拋出的異常對象時，如果`catch`語句的參數是引用型，則該參數直接引用到throw語句拋出的異常對象上；如果`catch`參數是傳值的，則拷貝構造一個新的對象作為`catch`語句的參數的值。在該catch語句結束時，先析構`catch`的傳值的參數對象，然後析構throw語句拋出的異常對象。

`catch`語句匹配異常對象時，不會做任何隱式類型轉換（implicit type conversion），包括類型提升（promotion）。異常對象與catch語句進行匹配的規則很嚴格，一般除了以下幾種情況外，異常對象的類型必須與catch語句的聲明類型完全匹配：允許非const到const的轉換；允許派生類到基類的轉換；將陣列和函數類型轉換為對應的指針。

在catch塊中可以使用不帶表達式的throw語句將捕獲的異常重新拋出：

```cpp
throw ;
```

被重新拋出的異常對象就是當前catch語句捕獲時所匹配的，原本由`throw`語句拋出的那個異常對象。重新拋出的異常對象與catch語句的形參無關。如原來拋出的是派生類Derived，catch語句形參是基類Based，則重新拋出後的異常類型是Derived。如果catch語句形參是引用型，重新拋出的原來的異常對象的內容可能已在catch語句內部被修改了。

可以用catch(...){ }來捕獲所有的異常。通常在catch(...){ }中，先執行可做的處理，然後重新拋出異常。

catch語句內部產生的新異常，或者「重新拋出異常」，均不能被同級的`try...catch...`中其他的catch語句捕獲、處理。只能由更外層的catch語句去捕獲該異常。

## 棧展開

棧展開（unwinding）是指當前的`try...catch...`塊匹配成功或者匹配不成功異常對象後，從`try`塊內異常對象的拋出位置，到`try`塊的開始處的所有已經執行了各自構造函數的局部變量，按照構造生成順序的逆序，依次被析構。如果當前函數內對拋出的異常對象匹配不成功，則從最外層的`try`語句到當前函數體的起始位置處的局部變量也依次被逆序析構，實現棧展開，然後再回退到調用棧的上一層函數內從函數調用點開始繼續處理該異常。

catch語句如果匹配異常對象成功，在完成了對catch語句的參數的初始化（對傳值參數完成了參數對象的copy構造）之後，對同層級的try塊執行棧展開。

由於線程執行時，被調用的函數的參數、返回地址、局部變量等都是依函數調用次序保存在函數調用棧（即線程運行時棧）上。當前被調用函數的參數、局部變量名字可以覆蓋掉早前調用函數的同名變量，看起來就是只有當前函數內的名字可以訪問，早前調用的函數內部的名字都不可訪問，就像磁帶被「捲起」。異常處理時按照函數調用順序的逆序析構，依次析構各個被調函數的局部變量，就類似把已經捲起的「磁帶」再展開，抹去上面記錄的數據，故此「棧展開」得名。unwinding在物理學、電工學上也翻譯做「退繞」、「退卷」。

## C++標準程序庫中定義的異常類

標準異常類定義在C++標準程序庫的四個頭文件中：

- 中定義了exception類
- 中定義了bad_alloc類
- 中定義了bad_cast類
- 中定義了runtime_error、logic_error類

所有的異常類都是exception類的子類。

runtime_error類（表示運行時才能檢測到的異常）包含了overflow_error、underflow_error、range_error幾個子類。

logic_error類（一般的邏輯異常）包含了domain_error、invalid_argument、out_of_range、length_error幾個子類。

各種標準異常類都定義了一個接受字串的構造函數，字串初始化式用於為所發生的異常提供更多的資訊。所有異常類都有一個what虛函數，它返回一個指向C風格字串的指針。

應用程式可以從各種標準異常類派生自己的異常類。

## 函數的異常規格

異常規格（exception specification）列出函數可能會拋出的所有異常的類型。異常規格寫在函數的形參表之後的關鍵字throw之後跟著一對圓括號括住的異常類型列表。如：

```cpp
void foo(int) throw(bad_alloc, invalid_argument)
{
/*函数体*/
}
```

異常列表還可以為空：

```cpp
void foo(int) throw(); 
```

表示該函數不拋出任何異常。

如果函數內拋出的異常的類型不在該函數的異常規格中，則系統函數unexpected被調用。如果在unexpected中拋出的異常出現在該函數的異常規格中，則在該函數被調用處恢復對異常的catch處理。如果在unexpected中拋出的異常不在該函數的異常規格中，則調用系統函數terminate以終止程序。

標準異常類中的構造函數、析構函數和what虛函數都承諾不拋出異常。如what的完整聲明為：virtual const char* what const throw;

派生類中的虛函數不能拋出基類虛函數中沒有聲明的新異常。

使用函數的異常規格的好處：

- 拋出一個對象而不是用函數返回值判斷是否出錯。
- 函數調用序列中間的函數可以不考慮異常處理，由某一層函數調用撲捉異常。
- unwinding自動發生。

Microsoft Visual C++接受但暫不支持C++標準中的函數的異常規格。即使使用了編譯器選項/D1ESrt，函數拋出不在其異常規格中的其他類型異常時，不會自動調用unexpected，而是在該函數調用點處的`try...catch...`處理。在Visual C++的函數名字修飾（name mangling）中，函數的形參的類型都編碼入被修飾後的函數名字中；但是函數的異常說明中的類型都沒有編碼入被修飾後的函數名字中。

## noexcept關鍵字

事實上，異常規格這一特性在程序中很少被使用，因此在C++11中被棄用。C++11定義了新的noexcept關鍵字。如果在函數聲明後直接加上noexcept關鍵字，表示函數不會拋出異常。另外一種形式是noexcept關鍵字後跟常量表達式，其值轉為布爾值，如果為真表示函數不會拋出異常，反之，則有可能拋出異常。

```
returnType funcDeclaration (args) noexcept(常量表达式) ;
```

如果保證不拋出異常的函數卻實際上拋出異常，則會直接調用std::terminate中斷程序的執行。

noexcept關鍵字還可以用作運算符，其後的操作數表達式如果有可能拋出異常，則運算符返回為false；如果操作數表達式保證不拋出異常，則運算符返回為true。這一運算符用於在定義模板函數時可以根據模板參數類型來確定是否傳出異常。

對類析構函數，使用noexcept關鍵字也可以顯式指明不剖出異常。類析構函數默認不拋出異常。如果聲明為（或默認）不拋出異常的類析構函數在運行時拋出了異常，將導致調用std::terminate中斷程序的執行。

## 構造函數、析構函數與異常

構造函數沒有返回值，所以應該用異常來報告發生的問題。構造函數拋出異常就意味著該構造函數沒有執行完，所以其對應的析構函數不會被自動調用，因此構造函數應該先析構所有已初始化的基對象、成員對象，再拋出異常。

析構函數被期望不向函數外拋出異常。析構函數中向函數外拋出異常，將直接調用terminator系統函數終止程序。如果一個析構函數內部拋出了異常，就應該在該析構函數內部捕獲、處理了該異常，不能讓異常被拋出析構函數之外。

### 構造函數初始化列表的異常機制

C++類構造函數初始化列表的異常機制，稱為function-try block。一般形式為：

```cpp
myClass::myClass(type1 pa1) 
    try:  _myClass_val (初始化值)  
{  
  /*构造函数的函数体 */
}  
  catch ( exception& err )  
{ 
  /* 构造函数的异常处理部分 */
};  
```

## 資源獲取即初始化

資源獲取即初始化（Resource acquisition is initialization，RAII）是指：為了更為方便、魯棒地釋放已獲取的資源，避免資源死鎖，一個辦法是把資源數據用對象封裝起來。程序發生異常，執行棧展開時，封裝了資源的對象會被自動調用其析構函數以釋放資源。

## 例子

```cpp
#include <iostream>
#include <exception>
using namespace std;

class AA{

private:	

public:
	int s;
	AA(class AA& rhs)
            {  cout<<"copy ctor;"<<endl; }
        AA(int ihs) 
                try:  s ( ihs )
	           { cout<<"defautl ctor;"<<endl; }
                catch(...)
                    { }
        ~AA()
               { cout<<"dtor:"<<s<<endl; }
	void foo()  throw ( exception )
	{
           throw 3.14; //浮点型异常对象不在该函数的异常规格中
        }
};
 
int main()
{   
       AA a(101);

        try
	{
                AA temp(102);
                temp.foo(); //对于Visual C++，即使用编译器选项/D1ESrt
                                   //仍然会无视AA::foo函数的异常规格
		throw a; 
	}
	catch (AA& e) //引用型参数，可改为传值型参数
	{
		e.s=102;  //修改throw抛出的异常对象
		cout<<"catch AA exception!"<<endl;
	} 
	catch(...)
	{
		cout<<"catch all others exception!"<<endl;
	}
}
```

## C++異常與Windows作業系統異常處理機制

在Windows作業系統上，編譯器實現的C++異常一般是基於作業系統的異常處理機制。

## 參考文獻

- 《ISO/IEC 14882:2011 C++ Standard》第15章 「Exception handling」