使用錯誤程式碼對象進行C++錯誤處理

  • 2020 年 4 月 10 日
  • 筆記

原文發表於codeproject,由本人翻譯整理分享於此。

前言

我已經使用了本文描述的程式碼和機制近20年了,到目前為止,我還沒有找到更好的方法來處理大型C++項目中的錯誤。最初的想法是從一篇文章(Dr Dobbs Journal 2000年)中摘錄出來的。我已經添加了一些新內容進去,使它更容易在生產環境中使用。

寫這篇文章的衝動是最近發表在Andrzej的C++部落格。正如我們在本文後面將看到的那樣,使用錯誤程式碼對象可以產生更清晰、更易於維護的程式碼。

背景

每個C++程式設計師都知道處理異常情況的傳統方法有兩種:第一種是從良好的舊C風格繼承而來,返回錯誤程式碼,並希望調用者進行判斷並採取適當的操作;第二種方法是拋出異常,並希望周圍程式碼塊捕獲並處理該異常。C++ FAQ強烈支援第二種方法,認為它會使得程式碼更安全。

然而,使用異常也有其自身的缺點。程式碼變得更加複雜,用戶必須知道所有可能引發的異常。這就是為什麼舊的C++規範在函數聲明中添加了「異常規範」。此外,異常會降低程式碼的效率。

錯誤程式碼對象被設計成類似於傳統C錯誤程式碼的函數返回。最大的區別是,如果不進行判斷,它們就會拋出異常。

讓我們舉個小例子,看看不同的實現會是什麼樣的。

首先,採用傳統錯誤碼的經典C方法:
int my_sqrt (float& value) {    if (value < 0)      return -1;    value = sqrt(value);    return 0;  }    int main () {    double val = -1;      // 注意,這裡已經進行了返回值得檢查    if (my_sqrt (val) == -1)      printf ("square root of negative number");      // 有些人會忘記返回值檢查    my_sqrt (val);      // 這時候斷言出錯,因為我們沒有檢查返回值    assert (val >= 0);  }  

如果不檢查結果,所有的壞事情都會發生,我們必須準備好使用所有傳統的調試工具來找出問題。

使用傳統C++異常,相同的程式碼可能如下所示:
void my_sqrt (float& value) {    if (value < 0)      throw std::exception ();    value = sqrt(value);  }    int main () {    double val = -1;      // 注意,這裡已經捕獲異常    try {      my_sqrt (val);    } catch (std::exception& x) {      printf ("square root of negative number");    }      // 有些人可能會忘記捕獲異常    my_sqrt (val);      // 這時候斷言出錯,因為我們沒有捕獲異常    assert (val >= 0);  }  

異常處理在這樣一個小例子中非常有用,因為我們可以看到my_sqrt函數使用try…catch包裹。但是,如果函數被深埋在庫中,你可能不知道它可能拋出哪些異常。請注意,從my_sqrt函數簽名中根本不知道它會拋出什麼異常(如果它有拋出異常的話)。

現在.……咳咳..……錯誤程式碼對象(erc)登場:
erc my_sqrt (float& value) {    if (value < 0)      return -1;    value = sqrt(value);    return 0;  }    int main () {    double val = -1;      // 注意,這裡進行返回值檢查    if (my_sqrt (val) == -1)                    // (1)      printf ("square root of negative number");      // 如果你喜歡異常處理,也是可以的    try {      my_sqrt (val);    } catch (erc& x) {      printf ("square root of negative number");    }      // 有些人可能忘記檢查返回值    my_sqrt (val);                              // (2)      // 程式會崩潰,因為有一個未捕獲的異常    assert (val >= 0);  }  

在深入了解這種方法的魔力之前,請先觀察幾點:

  • 首先,一個術語問題:為了區分傳統的「C」錯誤程式碼和我的錯誤程式碼對象,在本文的其餘部分,我將把「錯誤程式碼」稱為我的錯誤程式碼對象。當我需要引用傳統的「C」錯誤程式碼時,我將它們稱為「C錯誤程式碼」。
  • my_sqrt函數簽名清楚地指示它將返回錯誤程式碼。在C異常情況下,沒有跡象表明它會拋出異常。很久以前,C98有這些異常規範,但在C++11中就被廢棄了。你可以在雷蒙德·陳(Raymond Chen)的文章中找到更多關於這一點的討論(The sad history of the C++ throw(…) exception) specifier。C錯誤程式碼方案也沒有明確返回的整數值是錯誤程式碼。

初窺Error Code對象

我們先來一個全貌展示,暫時忽略一些細節,後續再細講。 當創建一個erc對象時,它有一個整數值(就像C錯誤程式碼)和一個活動標誌。

class erc  {  public:    erc (int val) : value (val), active (true) {};  //...  private:    int value;          // 一個整數值    bool active;        // 一個活動標誌  }  

如果釋放erc對象時,活動標誌被設置,則析構函數將會引發異常。

class erc  {  public:    erc (int val) : value (val), active (true) {}      // 析構函數檢查活動標誌,決定是否拋出異常    ~erc () noexcept(false) {if (active) throw *this;}  //...  private:    int value;    bool active;  }  

到目前為止,仍然沒有什麼特別之處:這僅僅是一個在析構函數中拋出異常的對象。也因為如此,我們必須使用noexcept(false)來修飾析構函數。

整數轉換運算符則返回erc對象的整數值,並重置活動標誌:

class erc  {  public:    erc (int val) : value (val), active (true) {}    ~erc () noexcept(false) {if (active) throw *this;}      // 整數轉換運算符,返回整數值,重置活動標誌    operator int () {active = false; return value;}    //...  private:    int value;    bool active;  }  

由於活動標誌已被重置,當erc對象超出作用域時,析構函數將不再拋出異常。通常,當對錯誤程式碼進行檢查時,將調用整數轉換運算符。

回顧一下前面簡單的用法示例,在標記為(1)的注釋算處,函數my_sqrt返回的erc對象與整數值進行比較,從而調用整數轉換運算符。因此,活動標誌將被重置,並且析構函數不會拋出異常。在標記為(2)的注釋處,函數my_sqrt返回的erc對象,由於設置了活動標誌,析構函數將引發異常。

遵循公認的Unix慣例,正如亞里士多德所說,成功的方法只有一種,那就是數值『0』表示成功。erc對象的數值為0則不拋出異常。任何其他數值都表示失敗,並拋出異常(如果沒有檢查返回值)。

這是錯誤程式碼對象的整個概念的精髓,如Dobbs Journal的文章所示。然而,我無法抗拒接受一個簡單的想法並使它變得更複雜的誘惑;繼續閱讀!

更多細節

前面只是全貌展示,忽略了一些細節。這些細節使錯誤程式碼功能更完善,便於把它集成到大型項目中。首先,我們需要一個移動構造函數和一個移動賦值操作符。目的是把活動標誌傳遞給新對象,並使原對象的活動標誌失效,確保只有一個活動的erc對象。

為了便於處理,我們還需要將錯誤程式碼分類的組件,這個組件是通過error facility對象(errfac)實現。除了數值和活動標誌屬性之外,Erc還具有一個facility對象和一個嚴重性級別。Erc析構函數並不像我們前面那樣直接拋出異常,而是調用errfacraise函數,與facility對象關聯起來。在這個raise函數中,比較erc對象的嚴重性級別和facility對象關聯的日誌級別。如果erc對象的級別高於facility對象的日誌級別,則errfacraise()函數調用errfac::log()函數生成錯誤資訊並拋出異常,或在超過預設級別時只記錄錯誤資訊。嚴重性級別是從UNIX syslog函數借用的:

名字 數值 動作
ERROR_PRI_SUCCESS 0 總是不記錄,不拋出
ERROR_PRI_INFO 1 默認不記錄,不拋出
ERROR_PRI_NOTICE 2 默認不記錄,不拋出
ERROR_PRI_WARNING 3 默認記錄,不拋出
ERROR_PRI_ERROR 4 默認記錄,拋出
ERROR_PRI_CRITICAL 5 默認記錄,拋出
ERROR_PRI_ALERT 6 默認記錄,拋出
ERROR_PRI_EMERG 7 總是記錄,拋出

默認情況下,錯誤程式碼與默認的facility對象關聯。但是,我們也可以定義不同的facility類,重新處理錯誤。例如,您可以為所有套接字錯誤定義一個專門的錯誤處理facility類,該類把錯誤程式碼轉換為有意義的消息。具有不同的錯誤級別有利於測試或調試,通過改變某一類錯誤的拋出或日誌記錄級別。

一個更實用的例子

這篇部落格文章前面提到的,一個HTTP客戶端程式的基本流程:

Status get_data_from_server(HostName host)  {    open_socket();    if (failed)      return failure();      resolve_host();    if (failed)      return failure();      connect();    if (failed)      return failure();      send_data();    if (failed)      return failure();      receive_data();    if (failed)      return failure();      close_socket(); // 有資源漏的可能    return success();  }  

這裡有個問題是,因為套接字沒有關閉函數就返回,會產生資源泄漏。在這種情況下,讓我們看看如何使用錯誤程式碼(指作者寫的Erc)。

如果我們想使用異常,程式碼可以如下所示:

// 函數聲明,返回值得使用erc  erc open_socket ();  erc resolve_host ();  erc connect ();  erc send_data ();  erc receive_data ();  erc close_socket ();    erc get_data_from_server(HostName host)  {    erc result;    try {      // 這些函數調用失敗,會觸發異常      open_socket ();      resolve_host ();      connect ();      send_data ();      receive_data ();    } catch (erc& x) {      result = x;         // 返回erc對象給外部調用者    }      close_socket ();      // 清理    return result;  }  

毫無例外,相同的程式碼可以寫成:

// 函數聲明,返回值使用erc  erc open_socket ();  erc resolve_host ();  erc connect ();  erc send_data ();  erc receive_data ();  erc close_socket ();    erc get_data_from_server(HostName host)  {    erc result;      (result = open_socket ())    || (result = resolve_host ())    || (result = connect ())    || (result = send_data ())    || (result = receive_data ());      close_socket ();      // 清理    result.reactivate ();    return result;  }  

在上面的片段中,result已轉換為整數,因為它必須參與邏輯或表達式。此轉換重置活動標誌,因此我們必須再次顯式打開它,方法是調用reactivate()功能。如果所有函數調用都是成功的,那麼結果就是0,而且,按照慣例它不會拋出異常。

最後

附件的源程式碼是高品質的、經過合理優化的,希望它不會更很難使用。演示項目是對流行的SQLITE資料庫的C++包裝器。演示項目比較大,因為它包含了SQLITE最新版本的程式碼(截至本文編寫時,2019年11月)。源程式碼和演示項目都包括 Doxygen文檔。

歷史

2019年11月12日:初版

源碼和演示項目

Download source code – 6.9 KB Download demo project – 2.2 MB

歡迎關注我的公眾號【林哥哥的編程札記】,也歡迎讚賞,謝謝!