C++ コア ガイドライン:例外処理に関する規則

今日の投稿は、例外をスローしてキャッチする正しい方法についてです。これは特に、例外をいつスローし、どのようにキャッチする必要があるかを意味します。

今日のルールは次のとおりです。

  • E.14:目的に合わせて設計されたユーザー定義型を例外として使用する (組み込み型ではない)
  • E.15:参照によって階層から例外をキャッチする
  • E.16:デストラクタ、解放、および swap 決して失敗してはならない
  • E.17:すべての関数ですべての例外をキャッチしようとしないでください
  • E.18:明示的な try の使用を最小限に抑える /catch

最初のものに直接ジャンプさせてください。

E.14:目的に合わせて設計されたユーザー定義型を例外として使用する(組み込み型ではない)

標準の例外タイプや組み込みタイプを例外として使用しないでください。ガイドラインからの 2 つの禁止事項は次のとおりです。

組み込み型

void my_code() // Don't
{
 // ...
 throw 7; // 7 means "moon in the 4th quarter"
 // ...
}

void your_code() // Don't
{
 try {
 // ...
 my_code();
 // ...
 }
 catch(int i) { // i == 7 means "input buffer too small"
 // ...
 }
}

この場合、例外はセマンティックを持たない単なる int です。 7 の意味はコメントに示されていますが、自己記述型の方が適切です。コメントは間違っている可能性があります。確かに、ドキュメントを調べてアイデアを得る必要があります。 kind int の例外に意味のある情報を付加することはできません。 7 をお持ちの場合は、例外処理に少なくとも 1 から 6 の数字を使用していると思います。 1 は不特定のエラーなどを意味します。これは非常に洗練されており、エラーが発生しやすく、読み取りと保守が非常に困難です。

標準例外

void my_code() // Don't
{
 // ...
 throw runtime_error{"moon in the 4th quarter"};
 // ...
}

void your_code() // Don't
{
 try {
 // ...
 my_code();
 // ...
 }
 catch(const runtime_error&) { // runtime_error means "input buffer too small"
 // ...
 }
}

組み込み型の代わりに標準例外を使用する方が優れています。例外に追加情報を添付したり、例外の階層を構築したりできるからです。これは良いですが、良くありません。なんで?例外が一般的すぎます。それはただのruntime_errorです。関数 my_code が入力サブシステムの一部であることをイメージしてください。関数の呼び出し元が std::runtime_error によって例外をキャッチした場合、それが「入力バッファーが小さすぎる」などの一般的なエラーなのか、「入力デバイスが接続されていません」などのサブシステム固有のエラーなのかわかりません。

これらの問題を克服するには、std::exception から特定の例外を派生させます。以下にアイデアを示す簡単な例を示します:

class InputSubSystemException: public std::exception{
 const char* what() const noexcept override {
 return "Provide more details to the exception";
 }
};

これで、入力サブシステムのクライアントは、catch(const InputSubSystemException&ex) を介して具体的に例外をキャッチできます。さらに、クラス InputSubSystemException からさらに派生させることで、例外階層を改良できます。

E.15:参照によって階層から例外をキャッチする

値による階層から例外をキャッチすると、スライスの被害者になる可能性があります。

InputSubSystemException (ルール E.14) から新しい例外クラス USBInputException を派生させ、タイプ InputSubSystemException の値によって例外をキャッチすると想像してください。ここで、タイプ USBInputException の例外がスローされます。

void subSystem(){
 // ...
 throw USBInputException();
 // ...
}

void clientCode(){
 try{
 subSystem();
 }
 catch(InputSubSystemException e) { // slicing may happen
 // ...
 }
}

USBInputException を InputSubSystemException の値でキャッチすることにより、スライスが開始され、e はより単純な型の InputSubSystemException を持ちます。スライスの詳細については、以前の記事「C++ コア ガイドライン:禁止事項に関する規則」を参照してください。

はっきり言うと:

<オール>
  • const 参照によって例外をキャッチし、例外を変更する場合は参照によってのみキャッチします。
  • 例外ハンドラーで例外 e を再スローする場合は、単に throw を使用し、e をスローしないでください。 2 番目のケースでは、e がコピーされます。
  • E.16:デストラクタ、解放、および swap 決して失敗してはならない

    このルールは非常に明白です。オブジェクトの破棄中に例外を処理する信頼できる方法がないため、デストラクタと割り当て解除はスローしないでください。

    スワップは、型のコピーおよび移動セマンティックを実装するための基本的なビルディング ブロックとしてよく使用されます。したがって、スワップ中に例外が発生した場合、初期化されていないか、完全に初期化されていないオブジェクトが残ります。 noexcept スワップの詳細については、C++ コア ガイドライン:比較、スワップ、およびハッシュを参照してください。

    try と except を適切に使用するための次の 2 つのルールは、非常によく似ています。

    E.17:すべての関数ですべての例外をキャッチしようとしないでください。E.18:明示的な try の使用を最小限に抑えてください /catch

    制御フローの観点から、try/catch は goto ステートメントと多くの共通点があります。これは、例外がスローされた場合、制御フローが、サブシステムのまったく別の機能にある可能性がある例外ハンドラーに直接ジャンプすることを意味します。最終的に、スパゲッティ コードが得られる場合があります。制御フローの予測と維持が困難なコードを意味します。

    最後に、ルール E.1 に戻ります。設計の早い段階でエラー処理戦略を策定します。

    ここでの質問は、例外処理をどのように構築する必要があるかということです。自問する必要があると思います:ローカルで例外を処理することは可能ですか?はいの場合は、実行してください。そうでない場合は、十分に処理できるようになるまで例外を伝播させます。サブシステムのクライアントを任意の例外から保護する必要があるため、多くの場合、サブシステムの境界は例外を処理するのに適した場所です。境界レベルでは、定期的および不定期的な制御フローで構成されるインターフェイスがあります。定期的な通信は、インターフェイスの機能面、またはシステムが何をすべきかです。不規則な通信は、非機能的な側面またはシステムがどのように実行されるべきかを表します。非機能的な側面の大部分は例外処理であるため、伝播された例外を処理する適切な場所です。

    次は?

    エラー処理の 6 つのルールは、C++ コア ガイドラインにまだ残っています。これらは、定数と不変性の規則に進む前の次の投稿のトピックです。


    No