C++11 的錯誤碼標準 part 1:error_code


<system_error> 這個函式庫是 C++11 時,加入標準函式庫裡的一個功能(C++ Referencecplusplus),主要是為了在 exception 以外,提供一個更有架構、同時也有擴展能力的錯誤回報機制。

為什麼不都用 exception 呢?主要是因為並不是所有的錯誤都是例外。
像是以網路的程式來說,有一些錯誤狀況,會是有相當高的發生機會、本來就應該要在程式的流程中被考慮到的、而不應該用例外來處理。

所以在這種狀況下,使用 exception 來處理這些錯誤狀況,並不見得是個好的方法。

<system_error> 最初是 Boost C++ Libraries 中的東西(參考),後來才被整合到 C++11 裡的,所以如果有在用 Boost 的話,對他應該不至於太陌生;像是在 Boost::ASIO(參考)裡面,主要就是透過 error_code 來做錯誤的回報。


<system_error> 中,也包含了好幾個類別,不過這邊就先從 std::error_code 開始看吧~

首先,在 C 語言中,比較常見的錯誤處理,都是透過回傳一個錯誤碼來代表執行的結果;而如果錯誤的類型較多的話,大多會用列舉型別、或是直接用 #define 的方法,來定義不同的錯誤代碼。

像是很久之前 Heresy 比較認真在玩的 OpenNI 基本上也是採用這樣的方法來作錯誤處理的(參考)。

但是透過這樣的作法,最常碰到的問題,就是在同時使用多個函式庫的時候,錯誤代碼會有重複定義、甚至是同一個錯誤代碼,代表不同意義的狀況…

std::error_code文件)為了避免這個問題,除了有錯誤碼之外,又加入了「category」來作區份,避免不同的函式庫會碰到的錯誤碼相同、但又代表不同意義的問題;同時,透過 std::error_category 的實作,也可以提供更詳細的錯誤說明。

另外,如果有需要的話,也可以直接把 std::error_code 當成一個 exception 丟出去,把它當成 exception 的形式來操作。
(但是似乎不能用 std::exception 來接…)


如果是要為自己的函式庫、定義一個相容於 std::error_code 的錯誤回報機制的話,這邊需要實作自己的 std::error_category

下面就是一個簡單的例子:

namespace Heresy
{
// define error code enum class ErrorCode { Success = 0, ErrorType1, ErrorType2 };
// define error_category class ErrorCategory : public std::error_category { public: ErrorCategory() = default; // map ErrorCode to detail message text std::string message(int c) const override { switch (static_cast<ErrorCode>(c)) { case ErrorCode::Success: return "Success"; case ErrorCode::ErrorType1: return "Error Type 1"; case ErrorCode::ErrorType2: return "Error Type 2"; } } // the name of this error_category const char* name() const noexcept override { return "Error Category by Heresy"; } public: // get referenceof shared ErrorCategory static const std::error_category& get() { const static ErrorCategory sCategory; return sCategory; } };
// convert ErrorCode to std::error_code std::error_code make_error_code(ErrorCode ec) { return std::error_code(static_cast<int>(ec), ErrorCategory::get()); }
void MyFunction(bool bError) { if(bError) throw make_error_code(ErrorCode::ErrorType1); } void MyFunction(bool bError, std::error_code& ec) { if(bError) ec = make_error_code(ErrorCode::ErrorType2); else ec = make_error_code(ErrorCode::Success); } }
namespace std { // let compiler know that Heresy::ErrorCode is compatible with std::error_code template <> struct is_error_code_enum<Heresy::ErrorCode> : true_type {}; }

下面則是程式碼內容的簡單介紹: 

ErrorCode

在這邊,列舉型別的 ErrorCode 就是用來定義錯誤的狀況列表的,這邊就簡單地定義三種狀態做示意。

這邊要注意的是,在定義的時候,最好把沒有問題(成功)的狀態定義成 0;因為 std::error_code 的運作有不少地方是以「是否為 0」來做處理的。

ErrorCategory

接下來的 ErrorCategory 則就是繼承自 std::error_category、根據自己的需求重新實作的類別;可以看到,他主要的功能其實就是透過 message() 這個函式,針對前面定義的 ErrorCode 提供詳細的文字說明。

由於在 std::error_code 中,是以常數指標的形式去連結到 std::error_category,所以 ErrorCategory 在整個程式中只需要有一個實例、以「單例模式」(Singleton)來設計會比較合適,所以這邊是透過 get() 這個靜態函式來回傳內部的靜態物件參考,讓所有 std::error_code 指到同一個 ErrorCategory

make_error_code()

而為了要方便之後的使用,還會需要另外定義 make_error_code() 這個函式,從 ErrorCode 建立出 std::error_code;可以看到,在裡面建立 std::error_code 的時候,就會去呼叫前面的 ErrorCategory::get()  這個函式。

std::is_error_code_enum<>

最後,為了要讓編譯器知道 ErrorCode 這個列舉型別可以轉換成 std::error_code,所以還必須要在 std 這個 namespace 下,將 is_error_code_enum<> 這個 template struct 針對 ErrorCode 做顯示特定化(explicit specialization),讓他變成是 std::true_type

如此一來,之後才能直接拿 std::error_code 來和 ErrorCode 做比較。

MyFunction()

另外,這邊也寫了兩個不同介面的 MyFunction(),算是不同使用形式的示意。

第一個是採用把 std::error_code 當作例外,用 thow 丟出來的形式來作錯誤回報;第二個則是透過把 std::error_code 當作函式的第二個參數,來作為回報狀態的機制。

而這邊建立 std::error_code 的法式去呼叫前面的 make_error_code() 這個函式,主要原因是因為程式的順序(還沒特化 is_error_code_enum<> ),所以沒辦法用自動轉換的。如果是在後面,或是拆成 .h/.cpp 的話,基本上可以不用去呼叫 make_error_code(),直接用 ErrorCode 就可以了。


而實際使用,如果是採用 try-catch 的機制的話,大概會像:

try
{
  Heresy::MyFunction(true);
}
catch (std::error_code & e)
{
  std::cout << e.message() << std::endl;
}

可以看到,這樣的使用方法,基本上是和 std::exception 幾乎一樣的~

個人覺得比較討厭的是,在 catch 的時候,似乎沒辦法用 std::exception 來接 std::error_code…如果真的用 std::exception 來接的話,雖然可以正確編譯,但是在執行階段會直接當掉;是哪邊要做什麼修改嗎?可能之後有空再花時間研究看看吧…

而如果是不想使用 try-catch 的話,則也可以用比較像是傳統的方法來做;這邊就是在呼叫 MyFunction() 的時候,把 std::error_code 的參考傳入,讓函式把狀態寫進去。

std::error_code ec;
Heresy::MyFunction(true,ec);

if (ec)
{
  std::cout << "Error : " << ec.message() << std::endl;
}
else
{
  std::cout << "Work fine" << std::endl;
}

就 Heresy 所知,Boost.ASIO 裡面有不少函式,就是採用這樣的介面設計。

當然,這邊也可以把它寫成把 std::error_code 回傳的形式,例如:

std::error_code MyFunction(bool bError)
{
  if (bError)
    return ErrorCode::ErrorType2;
  else
    return ErrorCode::Success;
}

至於要採用什麼形式,應該就是看個人需求了。

而如同前面也有提到的,如果都有正確撰寫的,自定義的 ErrorCode 也是可以直接被轉換成 std::error_code 來做比對的。

std::error_code ec = Heresy::MyFunction(true);
if (ec == Heresy::ErrorCode::ErrorType1)
{
  // do something
}
else if (ec == Heresy::ErrorCode::ErrorType2)
{
  // do something
}

到這邊應該也可以看到,實際上在使用的時候,和直接使用列舉型別或整數來定義錯誤代碼其實沒有太大的差異;ErrorCategory 是被完全隱藏起來、不需要去在意的, 

這部分完整可以編譯的程式碼,可以參考 Heresy 放在 GitHub 上的檔案:https://github.com/KHeresy/misc/blob/master/error_code_sample.cpp


除了 error_code 外,<system_error> 裡面還提供了一個相當接近的東西,叫做 error_condition文件)。

他和 error_code 非常地接近,感覺差別主要是意義上,和使用時機的不同。

就 Heresy 的理解(有誤請指正),兩者的差別主要是:

  • error_code 基本上是比較底層的錯誤碼,針對有可能會因為作業系統實作的不同
  • error_condition 則是和平台差異無關(platform-independent)的錯誤碼,主要是用來做比較用的

而兩者也是可以拿來做比較的,不過 error_codeerror_code 比較時,是確認是否完全相同;但是拿 error_codeerror_condition 必較的時候,則是確認兩者是否「等價」,這部分算是比較不一樣的地方。

而這部份就之後也機會再寫了。


在 Heresy 來看,std::error_code 提供了一個標準、通用的錯誤碼管理機制,如果要自己開發函式庫的話,使用這個架構,應該會讓函式庫的錯誤處理更為通用、並易於擴展。

但是相對的,由於回報的錯誤代碼變成是統一的型別,如果文件(或是標頭檔)沒有寫好的話,很容易讓使用者不知道拿到的 error_code 到底要和什麼東西來比較。

實際上,Heresy 剛接觸 Boost.ASIO 的時候,第一次看到 boost::error_code,就根本不知道該怎麼處理…

但是相對地,透過 error_code 來做為錯誤回報的方式,在 Heresy 來看,最大的好處就是可以擴展。

比如說當繼承一個既有的類別來衍生新的類別的時候,新的錯誤可以透過另外定義新的 error_category 來處理,而輸出的錯誤代碼也還是 error_code、不會影響到函式的介面;這點在 Heresy 來看,真的是相當實用的~


參考:


廣告

對「C++11 的錯誤碼標準 part 1:error_code」的想法

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.