エラーと例外の処理 (Modern C++)

最新の C++ のほとんどのシナリオでは、論理エラーとランタイム エラーの両方を報告および処理する方法として、例外を使用することが推奨されます。 これは特に、エラーを検出した関数からその処理方法を認識するためのコンテキストを持つ関数までの間に、複数の関数がスタックに含まれる可能性がある場合に当てはまります。 例外は、エラーを検出して情報を呼び出し履歴に渡すコードに関する、正しく定義された正式な方法を提供します。

プログラム エラーは通常 2 つのカテゴリに分類されます。1 つは、プログラミングの間違いにより生じる論理エラー ("範囲外のインデックス" エラーなど) で、もう 1 つはプログラマの種類に分ける通常は: 誤り、たとえば、「有効範囲外のインデックス」エラーのプログラムによる論理エラー、およびプログラマが制御できないランタイム エラー ("ネットワーク サービスが利用不可" エラーなど) です。 C スタイル プログラミングと COM では、特定の関数のエラー コードまたはステータス コードを表す値を返すか、エラーが報告されたかどうかを確認するすべての関数呼び出しの後に呼び出し元がオプションで取得する可能性があるグローバル変数を設定することで、エラー レポートが管理されます。 たとえば、COM プログラミングは HRESULT 戻り値を使用してエラーを呼び出し元に通知します。また、Win32 API には、呼び出し履歴により報告された最後のエラーを取得する GetLastError 関数があります。 どちらの場合も、呼び出し元がコードを認識し、適切に応答することにかかっています。 呼び出し元が明示的にエラー コードを処理しない場合、プログラムが警告なしにクラッシュしたり、不適切なデータで実行を続けて間違った結果が生成される可能性があります。

最新の C++ では、次の理由で例外が推奨されます。

  • 例外は、エラー状態の認識と処理を呼び出し元コードに強制します。 ハンドルされない例外は、プログラムの実行を停止します。

  • 例外は、エラーを処理できる呼び出し履歴内のポイントにジャンプします。 中間関数は、例外を伝達することができます。 他のレイヤーに合わせる必要はありません。

  • 例外のスタック アンワインド機構は、例外のスロー後に明確に定義された規則に従ってスコープ内のすべてのオブジェクトを破棄します。

  • 例外によって、エラーを検出したコードとエラーを処理するコードを明確な区別することができるようになります。

簡略化された次の例は、C++ で例外をスローしてキャッチするのに必要な構文を示しています。

 
#include <stdexcept>
#include <limits>
#include <iostream>
 
using namespace std;
class MyClass
{
public:
   void MyFunc(char c)
   {
      if(c < numeric_limits<char>::max())
         throw invalid_argument("MyFunc argument too large.");
      //...
   }
};

int main()
{
   try
   {
      MyFunc(256); //cause an exception to throw
   }
 
   catch(invalid_argument& e)
   {
      cerr << e.what() << endl;
      return -1;
   }
   //...
   return 0;
}

C++ の例外は、C# や Java などの言語と似ています。 try ブロックでは、例外がスローされた場合、種類が例外の種類と同じ、最初の関連する catch ブロックによりキャッチされます。 言い換えると、実行が throw ステートメントから catch ステートメントにジャンプします。 使用可能な catch ブロックが見つからない場合、std::terminate が呼び出されてプログラムが終了します。 C++ では、どの種類もスローされる可能性があります。ただし、std::exception から直接または間接的に派生した型をスローすることをお勧めします。 前の例では、例外の種類 invalid_argumentは、<stdexcept> ヘッダー ファイルの標準ライブラリで定義されます。 C++ には、例外がスローされた場合にすべてのリソースが解放されることを確証するための finally ブロックが用意されていません (必要ありません)。 スマート ポインターを使用する Resource Acquisition Is Initialization (RAII) の表現形式には、リソース クリーンアップのための必須機能が用意されています。 詳細については、「方法: 例外安全性に対応した設計をする」を参照してください。 C++ のスタック アンワインド機構については、「C++ での例外とスタック アンワインド」を参照してください。

基本的なガイドライン

堅牢なエラー処理は、どのプログラミング言語でも簡単ではありません。 例外には、適切なエラー処理をサポートする機能がいくつか用意されていますが、すべての処理を自動的に行うことはできません。 例外機構の利点を理解するため、コードをデザインするときに例外を念頭に置いてください。

  • 発生することのないエラーをチェックするには、アサートを使用します。 発生する可能性があるエラー (たとえば、パブリック関数のパラメーターにおける入力検証のエラーなど) をチェックするには、例外を使用します。 詳細については、「例外とアサーション」を参照してください。

  • 例外は、エラーを処理するコードが、1 つ以上の介在する関数呼び出しによりエラーを検出したコードから切り離されている可能性がある場合に使用します。 エラーを処理するコードが、エラーを検出したコードに密に結合されている場合は、パフォーマンスが重要なループで代わりにエラー コードを使用するかどうかを検討します。 例外を使用しない場合の詳細については、「When Not to Use Exceptions」を参照してください。

  • 例外をスローまたは伝達する可能性のある関数ごとに、strong 保証、basic 保証、nothrow (noexcept) 保証の 3 つの例外保証のいずれかを指定します。 詳細については、「方法: 例外安全性に対応した設計をする」を参照してください。

  • 値渡しで例外をスローし、参照渡しでそれらの例外をキャッチします。 処理できない例外をキャッチしないでください。 詳細については、「スローできる例外をキャッチすると (C++) のガイドライン」を参照してください。

  • C++11 で廃止された例外指定を使用しないでください。 詳細については、「例外指定と noexcept」を参照してください。

  • 標準ライブラリの例外の種類は、適用するときに使用します。 カスタム例外の種類は、例外クラス階層から取得します。 詳細については、「方法: 標準ライブラリの例外オブジェクトを使用する」を参照してください。

  • 例外がデストラクターまたはメモリ解放関数からエスケープしないようにしてください。

例外とパフォーマンス

例外がスローされない場合、例外機構によるパフォーマンスの低下はごくわずかです。 例外がスローされた場合、スタックの走査およびアンワインドによるパフォーマンスの低下は、関数呼び出しとほぼ同程度です。 try ブロックが入力された後に呼び出し履歴を追跡するには、追加のデータ構造が必要で、例外がスローされた場合にスタックをアンワインドするには追加の命令が必要です。 ただし、ほとんどの場合、パフォーマンスの低下とメモリ使用量の増加はそれほど大きくありません。 パフォーマンスに対する例外の悪影響は、メモリ制約が非常に大きいシステムでのみ大きくなる可能性があります。エラーが定期的に発生する可能性が高く、エラーを処理するコードがエラーを報告したコードに密に結合されている、パフォーマンスが重要なループでも大きくなる可能性があります。 いずれの場合も、プロファイリングや測定を行わずに例外の実際の影響を把握することは不可能です。 影響が大きくなるまれな場合でも、優れたデザインの例外ポリシーにより実現する正確さの向上、管理の容易さ、他の利点と比較することができます。

例外とアサーション

例外とアサーションは、プログラムのランタイム エラーを検出する 2 つの別個の機構です。 開発時にアサートを使用して、すべてのコードが正しい場合は true になることがない条件をテストします。 エラーはコード内に修正が必要な箇所があることを示しており、プログラムが実行時から回復する必要がある条件を表しているわけではないため、例外を使用してそのようなエラーを処理するポイントはありません。 デバッガーでプログラムの状態を検査できるように、アサートはステートメントで実行を停止します。例外は、該当する最初のキャッチ ハンドラーから実行を続行します。 コードが正しい場合でも実行時に発生する可能性があるエラー条件 ("ファイルが見つかりません" や "メモリ不足" など) をチェックするには、例外を使用します。回復によりログにメッセージが出力され、プログラムが終了するだけの場合でも、これらの条件から回復することができます。 必ず、例外を使用してパブリックの関数への引数をチェックしてください。 関数にエラーがない場合でも、ユーザーが渡す引数を完全に制御できないことがあります。

C++ 例外と Windows SEH 例外

C プログラムと C++ プログラムのどちらでも、Windows オペレーティング システムの構造化例外処理 (SEH) 機構を使用できます。 SEH の概念は、C++ 例外の概念と似ていますが、SEH は try と catchの代わりに __try、__except、__finally の各構成体を使用する点が異なります。 Visual C++ では、C++ 例外が SEH 用に実装されています。 ただし、C++ コードを記述するときは、C++ 例外構文を使用してください。

SEH の詳細については、「構造化例外処理 (C/C++)」を参照してください。

例外指定と noexcept

例外指定は、関数がスローする可能性がある例外を指定する方法として C++ に導入されました。 ただし、実際には例外指定に問題があることがわかったため、C++11 ドラフト標準では廃止されています。 例外がどの例外のエスケープも許可しないことを示す throw() を除き、例外指定を使用しないことをお勧めします。 種類が throw(type) の例外指定を使用する場合は、Visual C++ がいくつかの点で標準から逸脱していることに注意してください。 詳細については、「例外の仕様」を参照してください。 noexcept 指定子は、throw() の推奨される代替手段として C++11 に導入されました。

参照

概念

方法: 例外的なコードと非例外的なコードをインターフェイスで連結する

その他の技術情報

C++ へようこそ (Modern C++)

C++ 言語リファレンス

C++ 標準ライブラリ リファレンス