原文發表于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 = https://www.cnblogs.com/qinwanlin/p/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 = https://www.cnblogs.com/qinwanlin/p/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 = https://www.cnblogs.com/qinwanlin/p/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++例外情況下,沒有跡象表明它會拋出例外,很久以前,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物件時,活動標志被設定,則解構式將會引發例外,
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解構式并不像我們前面那樣直接拋出例外,而是呼叫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();
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
歡迎關注我的公眾號【林哥哥的編程札記】,也歡迎贊賞,謝謝!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/40795.html
標籤:C++
上一篇:翻轉字串里面的單詞


