翻訳者からの一言
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 {
注目すべきことがあります。 まず、障害の理由はどのサービスでも非常に似ていますが、異なる名前と異なる数値が割り当てられています。 これは、2つのサービスが2つの異なるチームによって独立して開発されたという事実によるものです。 また、同じ数値が報告したサービスに応じて、2つの完全に異なる条件を参照できることも意味します。
次に、名前が示すように、失敗の原因にはさまざまな原因があります。
- 環境:内部サービスの問題(リソースなど)。
- 誤解:2つのサービス間。
- ユーザー:リクエストで誤ったデータを提供しています。
- 運が悪いだけです。実際には間違いではありませんが、たとえば、すべての場所が売り切れたため、回答をユーザーに返すことはできません。
なぜこれらの異なるエラーコードが本当に必要なのですか? これらのエラーのいずれかが発生した場合、ユーザーからの現在のリクエストの処理を停止します。 彼にフライトを提供できない場合は、次の状況のみを強調します。
- 非論理的な要求を行いました。
- あなたの旅行のための既知の便はありません。
- このシステムには、理解できない問題がありますが、答えを出すことができません。
一方、内部監査やバグの検索の目的では、たとえば、どのシステムが障害を報告したか、実際に何が起こったかなど、ログに記録されるより詳細な情報が必要です。 整数でエンコードできます。 接続しようとしたポートや使用しようとしたデータベースなど、その他の詳細はおそらく個別に登録されるため、
int
エンコードされたデータで十分です。
std :: error_code
std::error_code
標準ライブラリは、このタイプの情報を正確に格納するように設計されています。ステータスを表す数値と、この数値に値が割り当てられる「ドメイン」です。 つまり、
std::error_code
はペア:
{int, domain}
です。 これはインターフェースに反映されます:
void inspect(std::error_code ec) { ec.value();
ただし、この方法で
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 {
[値]を
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)
この意味で
、言及されて
いる記事では、成功を示す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
を接続するには、次のことを確認する必要があります。
std::is_error_code<Errc>::value
はtrue
返しtrue
。- タイプ
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
についても学ぶことができまし
た 。