天天看點

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

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

前言#

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

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

背景#

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

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

錯誤代碼對象被設計成類似于傳統C錯誤代碼的函數傳回。最大的差別是,如果不進行判斷,它們就會抛出異常。

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

首先,采用傳統錯誤碼的經典C方法:

Copy

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) {

throw std::exception ();           

// 注意,這裡已經捕獲異常

try {

my_sqrt (val);           

} catch (std::exception& x) {

printf ("square root of negative number");           

// 有些人可能會忘記捕獲異常

// 這時候斷言出錯,因為我們沒有捕獲異常

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

現在.……咳咳..……錯誤代碼對象(erc)登場:

erc my_sqrt (float& value) {

return -1;           

// 注意,這裡進行傳回值檢查

if (my_sqrt (val) == -1) // (1)

printf ("square root of negative number");
           

// 如果你喜歡異常處理,也是可以的

my_sqrt (val);           

} catch (erc& x) {

printf ("square root of negative number");           

// 有些人可能忘記檢查傳回值

my_sqrt (val); // (2)

// 程式會崩潰,因為有一個未捕獲的異常

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

首先,一個術語問題:為了區分傳統的“C”錯誤代碼和我的錯誤代碼對象,在本文的其餘部分,我将把“錯誤代碼”稱為我的錯誤代碼對象。當我需要引用傳統的“C”錯誤代碼時,我将它們稱為“C錯誤代碼”。

my_sqrt函數簽名清楚地訓示它将傳回錯誤代碼。在C++異常情況下,沒有迹象表明它會抛出異常。很久以前,C++98有這些異正常範,但在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對象時,活動标志被設定,則析構函數将會引發異常。

erc (int val) : value (val), active (true) {}

// 析構函數檢查活動标志,決定是否抛出異常

~erc () noexcept(false) {if (active) throw *this;}

int value;

bool active;

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

整數轉換運算符則傳回erc對象的整數值,并重置活動标志:

// 整數轉換運算符,傳回整數值,重置活動标志

operator int () {active = false; return value;}

由于活動标志已被重置,當erc對象超出作用域時,析構函數将不再抛出異常。通常,當對錯誤代碼進行檢查時,将調用整數轉換運算符。

回顧一下前面簡單的用法示例,在标記為(1)的注釋算處,函數my_sqrt傳回的erc對象與整數值進行比較,進而調用整數轉換運算符。是以,活動标志将被重置,并且析構函數不會抛出異常。在标記為(2)的注釋處,函數my_sqrt傳回的erc對象,由于設定了活動标志,析構函數将引發異常。

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

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

更多細節#

前面隻是全貌展示,忽略了一些細節。這些細節使錯誤代碼功能更完善,便于把它內建到大型項目中。首先,我們需要一個移動構造函數和一個移動指派操作符。目的是把活動标志傳遞給新對象,并使原對象的活動标志失效,確定隻有一個活動的erc對象。

為了便于處理,我們還需要将錯誤代碼分類的元件,這個元件是通過error facility對象(errfac)實作。除了數值和活動标志屬性之外,Erc還具有一個facility對象和一個嚴重性級别。Erc析構函數并不像我們前面那樣直接抛出異常,而是調用errfac::raise函數,與facility對象關聯起來。在這個raise函數中,比較erc對象的嚴重性級别和facility對象關聯的日志級别。如果erc對象的級别高于facility對象的日志級别,則errfac::raise()函數調用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();

return failure();
           

connect();

return failure();
           

send_data();

return failure();
           

receive_data();

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;

// 這些函數調用失敗,會觸發異常
open_socket ();
resolve_host ();
connect ();
send_data ();
receive_data ();           
result = x;         // 傳回erc對象給外部調用者           

close_socket (); // 清理

return result;

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

// 函數聲明,傳回值使用erc

(result = open_socket ())

|| (result = resolve_host ())

|| (result = connect ())

|| (result = send_data ())

|| (result = receive_data ());

result.reactivate ();

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

最後#

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

曆史#

2019年11月12日:初版

源碼和示範項目#

Download source code - 6.9 KB

Download demo project - 2.2 MB

作者: qinwanlin

出處:

https://www.cnblogs.com/qinwanlin/p/12669347.html