C++11 的錯誤碼標準 part 2:error_condition

| | 0 Comments| 16:25
Categories:

這篇是延續之前的《C++11 的錯誤碼標準:error_code》一文,繼續來講 <system_error> 裡的另一個類別、std::error_condition文件)。

不過老實說,Heresy 在繼續寫這篇的時候,才發現之前對於這邊的設計有些誤解,所以其實在 error_code 這篇文章有的內容可能不夠完整,預計會在用第三篇文章繼續補齊。

error_conditionerror_code 非常地接近,不管是實際儲存的資料,或是介面的設計,基本上都沒有太大的差異;兩者的差別,主要是概念上、使用時機的差別。

兩者的差異,主要是:

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

什麼時候會出現平台差異的問題呢?這邊用 C++17 標準函式庫的 filesystem(參考)來舉的例子:

std::error_code ec;
std::filesystem::copy_file("aaa", "bbb", ec);
if (ec)
  std::cout << ec << "n" << ec.message() << std::endl;

上面的程式碼,是把「aaa」這個檔案,複製一份成「bbb」。

這邊為了出現錯誤,所以是假設「bbb」已經存在;而這時候,就會發生錯誤,所以會透過 coutec 的錯誤訊息輸出。

在 Windows 平台上,錯誤會是:

system:80
檔案存在。

但是在 Linux 環境的話,他的錯誤會是:

generic:17
File exists

可以看到,雖然這邊是同樣一個錯誤狀況,但是因為平台的不同,所以拿到的 error_code 不管是 error_category 或是錯誤代碼,都是不一樣的。

在這種狀況下,如果想要針對不同的錯誤來做例外處理,就會變得相對麻煩,得針對不同的平台,來撰寫不同的條件判斷才行。

而和平台差異性無關的 error_condition 就是為了解決這個問題而設計的。

實際上,在 <system_error> 裡,就有針對 POSIX 的錯誤代碼,定義了 std::errc文件),來做為跨平台的比對基準。

以上面的例子來說,要去檢查是否是因為檔案已存在而出錯的話,寫成

if (ec == std::errc::file_exists)
{
  // do something
}

的話,就可以跨平台適用了~

而實際上在運作的時候,std::errc 是會先被轉型成 std::error_condition,然後再和 ec 這個 error_code 做比較;而且,這個時候的比較並不是去確認兩者是否完全相等,而是確認兩者是否等校(equivalent)。


在 Heresy 來看,一般狀況下,如果是要在自己的函式庫裡透過 error_code 來回報錯誤的話,其實大部分的時候,應該是可以不用去在乎 error_condition 的。

在什麼時候才會需要去實作 error_condition 的部分呢?在《System error support in C++0x – part 5 》這篇文章裡面,提出了三種使用情境:

  • 把作業系統層的錯誤抽象化
    • 前面舉的例子,應該就是這樣的應用。
      標準函式庫就是透過 std::errc 來做系統回報的錯誤代碼的抽象層。
  • 將一般性的 error code 賦予符合應用的意義
    • 當撰寫應用的時候,系統回報的錯誤代碼意義可能和應用的意義不符合,可以透過定義新的 error_condition,讓他和系統回報的錯誤等價,藉此來讓他在名稱上更為符合實際意義。
    • 文章中給的例子,是一個用檔案來實作資料庫的例子。
      當找不到指定項目的時候,系統回報的錯誤應該會是 ENOENT,要用 std::errc::no_such_file_or_directory 來做比較。
      但是對於使用者來說,讀取資料庫的資料會要確認是不是沒有檔案,其實是不太符合意義的;所以這時候可以另外定義一個 error_condition no_such_entry、讓他和 ENOENT 等價,這樣在使用上的意義會更為符合。
  • 對相關的錯誤集合做測試
    • 為了當有問題的時候能夠確實追蹤錯誤狀況,所以 error_code 通常會設計成盡量使用原始的錯誤代碼、並盡量區分地清楚;但是這樣的情況下,使用者對於同類型的錯誤,在比對時可能就得針對大量不同的錯誤做比對,造成開發上的麻煩。
      這時候就可以透過定義 error_condition、讓他和同類型的錯誤等價,這樣就只需要比對得到的 error_code 和這個 error_condition 是否等價就可以了。
    • 文章舉的例子,是比如說系統會回報的錯誤可能有 not_enough_memoryresource_unavailable_try_againtoo_many_files_opentoo_many_files_open_in_system 這四個,雖然細節上不盡相同,但是比較粗略地來看,其實都是「資源不足」的狀況;而如果想要簡化比對的流程,不想每次都要去比對這四個的話,那可以另外定義一個 error_condition low_system_resources、讓他和前面四個錯誤代碼等價,這樣以後只要比對一個東西就可以了。

而實作 error_condition 的方法,其實和 error_code 很像,也是要透過 error_category 來做;在大部分的流程上,都和前面的《C++11 的錯誤碼標準:error_code》一文中的類似。

不同的地方,在於要定義對應 error_conditionerror_category 的時候,需要另外定義用來判斷是否等價的函式。

當拿 error_codeerror_condition 作比對的時候,基本上是要靠 error_categoryequivalent() 這個函式來進行的;而他有兩個版本,對應不同的比對方向:

  • bool equivalent( int code, const error_condition& condition ) const
    • 確認本身的 error_code 是否和其他來源的 error_condition 是否等價
    • 定義上等同於 default_error_condition(code) == condition
  • bool equivalent( const error_code& code, int condition ) const
    • 確認本身的 error_condition 是否和其他來源的 error_code 是否等價
    • 定義上等同於 *this == code.category() && code.value() == condition

如果是要讓自己客製化的 error_code 可以相容於其他的既有的 error_condition 的話,基本上就是要重新定義第一個 equivalent();或者,比較簡單的狀況,也可以靠定義 default_error_condition() 這個函式來做到。

這部分,主要應該是要讓 error_code 可以搭配 error_condition 時,必須要去時作的。

而如果是要定義自己的 error_condition 的話,則就是要去重新定義第二個 equivalent() 的內容,自己做判斷是否等價了。

所以,如果是要定義自己的 error_condition 的話,會需要:

  1. 用列舉型別定義對應 error_condition 的錯誤代碼
  2. 定義自己的 error_category
    1. 需要實作 name()message()equivalent() 這三個函式
  3. 讓列舉型別的錯誤代碼可以轉換成 error_condition 
    1. 定義 make_error_condition()
    2. 定義 is_error_condition_enum<>

至於範例程式的部分,由於沒想到什麼好例子,所以這邊假設現在是想定義一個 error_condition,來簡化和標準函式庫的錯誤的比對。

下面就是定義了一個 ErrorCondition,裡面定義了 FileErrorNetworkError 來對應部分 std::errc 的內容:

namespace Heresy
{
enum class ErrorCondition { FileError = 1, NetworkError };
// define error_category class ErrorCategory : public std::error_category { public: std::string message(int c) const override { switch (static_cast<ErrorCondition>(c)) { case ErrorCondition::FileError: return "File-related error"; case ErrorCondition::NetworkError: return "network related error"; default: return "Undefined"; } } const char* name() const noexcept override { return "Error condition category by Heresy"; }
bool equivalent(const std::error_code& ec, int iCond) const noexcept override { switch (static_cast<ErrorCondition>(iCond)) { case ErrorCondition::FileError: return (ec == std::errc::file_exists || ec == std::errc::file_too_large || ec == std::errc::no_such_file_or_directory || ec == std::errc::read_only_file_system); case ErrorCondition::NetworkError: return (ec == std::errc::network_down || ec == std::errc::network_reset || ec == std::errc::network_unreachable); } return false; }
public: static const std::error_category& get() { const static ErrorCategory sCategory; return sCategory; } };
std::error_condition make_error_condition(ErrorCondition ec) { return std::error_condition(static_cast<int>(ec), ErrorCategory::get()); }
}
namespace std { // let compiler know that Heresy::ErrorCode is compatible with std::error_condition template <> struct is_error_condition_enum<Heresy::ErrorCondition> : true_type {}; }

ErrorCategory 這個繼承 std::error_category 的類別裡面,總共 override 了三個函式,其中 name() 是回傳自己的名稱,message() 則是將 ErrorCondition 轉換成文字的訊息。

不過,由於 message() 主要是被 std::error_condition::message() 呼叫的,但是一般來說並不會真的直接使 std::error_condition 的型別,所以在正常的使用方法下、基本上不太會被呼叫到,甚至可以直接回傳一個固定字串就好了。

這邊比較重要的就是 equivalent() 這個函式。這邊可以看到,在裡面是針對 FileErrorNetworkError 這兩個狀況,個別去比對傳進來的 error_code 是否和對應的 std::errc 的內容相符。

在這邊這個寫法,基本上就是讓 FileErrorstd::errcfile_existsfile_too_largeno_such_file_or_directoryread_only_file_system 這四個錯誤等價;而 NetworkError 則是和 network_xxx 等價。

而根據實際的需求,這邊也可以做更複雜的比對,來滿足自己的需要。

其他部分,則是還要定義 make_error_condition() 來把 ErrorCondition 轉換成 error_condition;並且要將 is_error_condition_enum<> 針對 ErrorCondition 顯示特定化(explicit specialization), 讓編譯器知道 ErrorCategory 可以自動轉型成 error_condition


當這邊都準備好之後,就可以用 ErrorCondition 跟系統回報的 error_code 比對了~

std::error_code ec = std::make_error_code(std::errc::file_exists);
if (ec == Heresy::ErrorCondition::FileError)
  std::cout << ec.message() << " is a kind of file error";
else
  std::cout << ec.message() << " is not a kind of file error";

像以上面的程式碼來說,系統就會認為 ecHeresy::ErrorCondition::FileError 等價了~

這邊完整的範例可以參考 GitHub 上的檔案:https://github.com/KHeresy/misc/blob/master/error_condition_example.cpp


感覺上,對於一般函式庫的開發者來說,就算決定要在函式庫中引進 error_code 作為錯誤回報的機制,error_condition 應該也算是不一定要實作的部分,而是視需求、有必要再去追加的功能了。

而對於函式庫的使用者來說,在使用 error_code 的架構來處理錯誤代碼的時候,error_categoryerror_condition 基本上都是隱藏起來,不會實際看到;甚至在實作的時候,開放的 header 檔中,也可以讓使用者只看到定義來代表錯誤的列舉型別。

所以實際上,在定義好這些元件後,對於函式庫的使用者,如果熟系 error_code 的概念的話,某種意義上其實在使用上好像還滿接近直接把列舉型別當作錯誤碼的。


Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *