独自のstd :: code_error

翻訳者からの一言



RuNetのトピックstd::system_error



を引き続き取り上げ、AndrzejKrzemieńskiブログのいくつかの記事を翻訳することにしました。これは前の投稿のコメントでアドバイスされました。



これらの記事は十分な量であるため、前回のようにそれらをヒープにマージするのではなく、元の形式で公開することにしました。



また、AndrzejKrzemieńskiにはかなり混oticとしたプレゼンテーションスタイルがありますが、これは編集しませんでした。 それでも、私は編集者ではなく翻訳者として行動します。 そのため、いくつかのポイントを理解するために2回読み直さなければならない場合があります。



はじめに



最近、私のアプリケーションで、 std::error_code



functionalによって提案された「エラー条件の分類」を実装しました。 この投稿では、私の経験を共有したいと思います。



C ++ 11には、エラー条件を分類するためのかなり複雑なメカニズムがあります。 「エラーコード」、「エラー状態」、「エラーカテゴリ」などの概念に出くわすかもしれませんが、それらがどれほど優れているか、そしてそれらをどのように使用するかを見つけるのは簡単ではありません。 このテーマに関するインターネット上の唯一の貴重な情報源は、 Boost.Asioライブラリの著者であるChristopher Kohlhoffからの一連の記事です。





[翻訳者注:以前、このサイクルを既にhabrに翻訳しました]



そして、それは私にとって本当に良いスタートでした。 しかし、それでも、このトピックについていくつかの情報源といくつかの説明があると便利だと思います。 それでは始めましょう...



問題



まず、なぜこれが必要なのですか。 フライトを検索するサービスがあります。 あなたはどこに、どこに飛びたいかを教えてくれ、特定のフライトと価格を提供します。 これを行うために、私のサービスは他のサービスに切り替えます:





これらの各サービスは、いくつかの理由でエラーを返す場合があります。 失敗の理由(サービスごとに異なる)をリストできます。 たとえば、これら2つのサービスの作成者は、次の列挙を選択しました。



 enum class FlightsErrc { //  0 NonexistentLocations = 10, //     DatesInThePast, //      InvertedDates, //    NoFlightsFound = 20, //      ProtocolViolation = 30, // ,  XML ConnectionError, //      ResourceError, //     Timeout, //     }; enum class SeatsErrc { //  0 InvalidRequest = 1, // ,  XML CouldNotConnect, //      InternalError, //     NoResponse, //     NonexistentClass, //     NoSeatAvailable, //    };
      
      





注目すべきことがあります。 まず、障害の理由はどのサービスでも非常に似ていますが、異なる名前と異なる数値が割り当てられています。 これは、2つのサービスが2つの異なるチームによって独立して開発されたという事実によるものです。 また、同じ数値が報告したサービスに応じて、2つの完全に異なる条件を参照できることも意味します。



次に、名前が示すように、失敗の原因にはさまざまな原因があります。





なぜこれらの異なるエラーコードが本当に必要なのですか? これらのエラーのいずれかが発生した場合、ユーザーからの現在のリクエストの処理を停止します。 彼にフライトを提供できない場合は、次の状況のみを強調します。



  1. 非論理的な要求を行いました。
  2. あなたの旅行のための既知の便はありません。
  3. このシステムには、理解できない問題がありますが、答えを出すことができません。


一方、内部監査やバグの検索の目的では、たとえば、どのシステムが障害を報告したか、実際に何が起こったかなど、ログに記録されるより詳細な情報が必要です。 整数でエンコードできます。 接続しようとしたポートや使用しようとしたデータベースなど、その他の詳細はおそらく個別に登録されるため、 int



エンコードされたデータで十分です。



std :: error_code



std::error_code



標準ライブラリは、このタイプの情報を正確に格納するように設計されています。ステータスを表す数値と、この数値に値が割り当てられる「ドメイン」です。 つまり、 std::error_code



はペア: {int, domain}



です。 これはインターフェースに反映されます:



 void inspect(std::error_code ec) { ec.value(); //  ec.category(); //  }
      
      





ただし、この方法でstd::error_code



を確認する必要はほとんどありません。 すでに述べたように、やりたいことは、作成された状態std::error_code



を予約し(アプリケーションの上位レベルによるその後の変換なし)、それを使用して特定の質問に答えることです。たとえば、「このエラーはユーザーによって引き起こされました。間違ったデータを提供していますか?」



例外の代わりにstd::error_code



使用する理由を自問している場合、明確にしましょう。これら2つのことは相互に排他的ではありません。 例外を介してプログラムのクラッシュを報告したい。 例外の内部では、文字列の代わりに、簡単に確認できるstd::error_code



を保存します。

std::error_code



は、例外を取り除くこととは関係ありません。 また、私のユースケースでは、多くの異なるタイプの例外を持つ正当な理由はありません。 それらを1つ(または2つ)の場所でのみキャッチし、 std::error_code



オブジェクトを確認することでさまざまな状況について学習します。



リスティングを接続する



次に、上記のフライトサービスからのエラーを保存できるように、 std::error_code



を適応させます。



 enum class FlightsErrc { //  0 NonexistentLocations = 10, //     DatesInThePast, //      InvertedDates, //    NoFlightsFound = 20, //      ProtocolViolation = 30, // ,  XML ConnectionError, //      ResourceError, //     Timeout, //     };
      
      





[値]をenum



からstd::error_code



に変換できるはずstd::error_code







 std::error_code ec = FlightsErrc::NonexistentLocations;
      
      





ただし、列挙は1つの条件を満たしている必要があります。数値0は誤った状況であってはなりません。 0は、任意のドメイン(カテゴリ)でのエラーの成功を表します。 この事実は、後でstd::error_code



オブジェクトをチェックするときに使用されます。



 void inspect(std::error_code ec) { if(ec) // : 0 != ec.value() handle_failure(ec); else handle_success(); }
      
      





この意味で、言及されいる記事では、成功を示す200という数値が誤って使用されています。



そのため、 FlightErrc



ようにしましたFlightErrc



列挙を0から開始しませんでした。これは、列挙された値のいずれにも一致しない列挙を作成できることを意味します。



 FlightsErrc fe {};
      
      





これは、C ++の列挙の重要な特性です(C ++ 11の列挙クラスも含む)。列挙範囲外の値を作成できます。 このため、列挙値ごとにcase



ラベルがあるcase



でも、コンパイラはswitch-statement



で「すべての制御パスが値を返すわけではない」という警告を発行します。



変換に戻ると、 std::error_code



は、次のような変換コンストラクターテンプレートがあります。



 template<class Errc> requires is_error_code<Errc>::value error_code(Errc e) noexcept: error_code{make_error_code(e)} {}
      
      





(もちろん、まだ存在していない概念的な構文を使用しましたが、考え方は明確である必要があります。このコンストラクターは、 std::is_error_code<Errc>::value



true



評価されるtrue



のみ使用可能true



。)



このコンストラクタは、システムでプラグインユーザー列挙を構成するように設計されています。 FlightErrc



を接続するには、次のことを確認する必要があります。



  1. std::is_error_code<Errc>::value



    true



    返しtrue



  2. タイプFlightsErrc



    make_error_code



    取るmake_error_code



    関数FlightsErrc



    定義されており、 ADLを介してアクセスできます。


最初の段落に関しては、標準テンプレートを特化する必要があります。



 namespace std { template<> struct is_error_code_enum<FlightsErrc>: true_type {}; }
      
      





これは、 std



何かを宣言することが「合法」である状況の1つです。



2番目のポイントに関しては、 make_error_code



と同じ名前空間でmake_error_code



関数のオーバーロードを宣言するだけです。



 enum class FlightsErrc; std::error_code make_error_code(FlightsErrc);
      
      





そして、これがプログラム/ライブラリの他の部分を見る必要があり、ヘッダーファイルで提供しなければならないことです。 残りはmake_error_code



関数の実装であり、別の翻訳単位(.cppファイル)に入れることができます。



この場所から、 FlightsErrc



error_code



と想定できerror_code







 std::error_code ec = FlightsErrc::NoFlightsFound; assert(ec == FlightsErrc::NoFlightsFound); assert(ec != FlightsErrc::InvertedDates);
      
      





エラーカテゴリの宣言



これまで、 error_code



はペアであると述べました: {number, domain}



、最初の要素はドメイン内の特定のエラー状況を一意に識別し、2番目は考えられるすべてのエラードメインの中でエラードメインを一意に識別します。 しかし、このドメイン識別子を1つのマシンワードに格納する必要がある場合、現在市場に出ているすべてのライブラリと今後登場するライブラリで一意になるようにするにはどうすればよいでしょうか。 実装の詳細としてドメイン識別子を非表示にします。 独自のエラーの列挙で別のサードパーティライブラリを使用する場合、それらのドメイン識別子が私たちのものと等しくないことをどのように保証できますか?



std::error_code



選択されたソリューションは、各グローバルオブジェクト(またはより正式には名前空間領域のオブジェクト)に一意のアドレスが割り当てられるという観察に基づいています。 ライブラリの数とグローバルオブジェクトの数に関係なく、各グローバルオブジェクトには一意のアドレスがあります。これは完全に明白です。



これを利用するには、 error_code



をシステムに接続する各タイプに一意のグローバルオブジェクトを関連付け、そのアドレスを識別子として使用する必要があります。 これで、ドメインを表すためにアドレスが使用されます。これは、まさにstd::error_code



が行うことです。 しかし、今、 *



を保存すると、



がどうあるべきかという疑問が生じます。 かなり合理的な選択:追加の利点を提供できるタイプを使用しましょう。 そのため、 T



タイプT



std::error_category



であり、追加の利点はそのインターフェースにあります。



 class error_category { public: virtual const char * name() const noexcept = 0; virtual string message(int ev) const = 0; //    ... };
      
      





「ドメイン」という名前を使用しましたが、標準ライブラリは同じ目的で「エラーのカテゴリ」という名前を使用しています。



既に何かを提供している純粋な仮想メソッドがあります std::error_category



継承したクラスへのポインターを保存します。エラーの新しい列挙ごとに std::error_category



継承した新しいクラスを取得する必要があります。 通常、純粋な仮想メソッドを作成するには、ヒープにオブジェクトを割り当てる必要がありますが、そのようなことは行いません。 グローバルオブジェクトを作成し、それらをポイントします。



std::error_category



は、他の場合に設定する必要がある他の仮想メソッドがありますが、 FlightErrc



を接続するためにこれを行う必要はありません。



ここで、 std::error_category



派生したクラスで表されるエラーのユーザー定義の「ドメイン」 std::error_category



、2つのメソッドをオーバーライドする必要があります。 name



メソッドは、エラーのカテゴリ(ドメイン)の短いニーモニック名を返します。 message



メソッドは、このドメインの各エラー値にテキストの説明を割り当てます。 これをFlightsErrc



するために、リストFlightsErrc



エラーのカテゴリを定義しましょう。 このクラスは1つの翻訳単位でのみ表示されることに注意してください。 他のファイルでは、そのインスタンスのアドレスを使用します。



 namespace { struct FlightsErrCategory: std::error_category { const char * name() const noexcept override; std::string message(int ev) const override; }; const char * FlightsErrCategory::name() const noexcept { return "flights"; } std::string FlightsErrCategory::message(int ev) const { switch(static_cast<FlightsErrc>(ev)) { case FlightsErrc::NonexistentLocations: return "nonexistent airport name in request"; case FlightsErrc::DatesInThePast: return "request for a date from the past"; case FlightsErrc::InvertedDates: return "requested flight return date before departure date"; case FlightsErrc::NoFlightsFound: return "no filight combination found"; case FlightsErrc::ProtocolViolation: return "received malformed request"; case FlightsErrc::ConnectionError: return "could not connect to server"; case FlightsErrc::ResourceError: return "insufficient resources"; case FlightsErrc::Timeout: return "processing timed out"; default: return "(unrecognized error)"; } } const FlightsErrCategory theFlightsErrCategory {}; }
      
      





name



メソッドは、 std::error_code



をログなどにストリーミングするときに使用される短いテキストを提供します。たとえば、これはエラーの原因を特定するのに役立ちます。 テキストはすべてのエラーの列挙で一意である必要はありません。最悪の場合、ジャーナルのエントリはあいまいになります。



message



メソッドは、カテゴリのエラーである数値の説明テキストを提供します。 これは、ログをデバッグまたは表示するときに役立ちます。 ただし、追加の処理を行わずにこのテキストをユーザーに表示することはおそらくないでしょう。



通常、このメソッドは直接呼び出されません。 呼び出し元は数値がFlightErrc



であることを知ることができないため、明示的にFlightErrc



キャストする必要があります。 static_cast



がないため、 上記の記事の例はコンパイルされないとstatic_cast



ます。 型キャスト後、列挙ではない値をチェックするリスクがあります。したがって、 default



ラベルが必要default







最後に、 FlightErrCategory



型のグローバルオブジェクトを初期化したことに注意してください。 これは、プログラム内でこのタイプの唯一のオブジェクトになります。 アドレスが必要になりますが、ポリモーフィックプロパティも使用します。



std::error_category



クラスはリテラル型ではありませんが、 constexpr



コンストラクターがありconstexpr



FlightErrCategory



クラスの暗黙的に宣言されたデフォルトコンストラクターは、このプロパティを継承します。 したがって、 この記事で説明するように、定数の初期化中にグローバルオブジェクトの初期化が実行されることを保証します 。したがって、静的初期化の順序に関する問題は含まれません。



さて、最後の欠けている部分はmake_error_code



の実装です:



 std::error_code make_error_code(FlightsErrc e) { return {static_cast<int>(e), theFlightsErrCategory}; }
      
      





これで完了です。 FlightsErrc



は、 std::error_code



ように使用できます。



 int main() { std::error_code ec = FlightsErrc::NoFlightsFound; std::cout << ec << std::endl; }
      
      





このプログラムの出力は次のようになります。



フライト:20


上記のすべてを示す完全に機能する例はここで見つけることができます



今日は以上です。 std::error_code



オブジェクトで有用なクエリを作成する方法についてはまだ検討していませんが、これは別の記事のトピックになります。



謝辞



std::error_code



std::error_code



背後にあるアイデアを説明してくれたことに感謝します。 Christopher Kohlhoffからの一連の記事に加えて、Niall DouglasのOutcomeライブラリのドキュメント( ここ)からstd::error_code



についても学ぶことができまし



All Articles