例外とエラー処理に関する最新の C++ のベスト プラクティス

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

例外的なコードに例外を使用する

プログラム エラーは、多くの場合、次の 2 つのカテゴリに分類されます。

  • プログラミングの間違いによって発生するロジック エラー。 たとえば、"範囲外のインデックス" エラーです。
  • プログラマが制御できる範囲を超えるランタイム エラー。 たとえば、"ネットワーク サービスを使用できません" というエラーです。

C スタイル プログラミングと COM では、特定の関数のエラー コードまたはステータス コードを表す値を返すか、エラーが報告されたかどうかを確認するすべての関数呼び出しの後に呼び出し元がオプションで取得する可能性があるグローバル変数を設定することで、エラー レポートが管理されます。 たとえば、COM プログラミングは HRESULT 戻り値を使用してエラーを呼び出し元に通知します。 また、Win32 API には、呼び出し履歴により報告された最後のエラーを取得する GetLastError 関数があります。 どちらの場合も、呼び出し元がコードを認識し、適切に応答することにかかっています。 呼び出し元が明示的にエラー コードを処理しない場合、プログラムが警告なしにクラッシュする場合があります。 また、不適切なデータで実行を続けて間違った結果が生成される可能性があります。

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

  • 例外は、エラー状態の認識と処理を呼び出し元コードに強制します。 ハンドルされない例外は、プログラムの実行を停止します。
  • 例外は、エラーを処理できる呼び出し履歴内のポイントにジャンプします。 中間関数は、例外を伝達することができます。 他のレイヤーに合わせる必要はありません。
  • 例外のスタック アンワインド機構は、例外のスロー後に明確に定義された規則に従ってスコープ内のすべてのオブジェクトを破棄します。
  • 例外によって、エラーを検出したコードとエラーを処理するコードを明確な区別することができるようになります。

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

#include <stdexcept>
#include <limits>
#include <iostream>

using namespace std;

void MyFunc(int 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++ のスタックアンワインド機構の詳細については、「例外とスタック アンワインド」をご覧ください。

基本的なガイドライン

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

  • アサートを使用して、常に true または常に false にする必要がある条件を確認します。 発生する可能性があるエラー (たとえば、パブリック関数のパラメーターにおける入力検証のエラーなど) をチェックするには、例外を使用します。 詳細については、「例外とアサーション」セクションをご覧ください。
  • 例外は、エラーを処理するコードが、1 つ以上の介在する関数呼び出しによりエラーを検出したコードから切り離されている場合に使用します。 エラーを処理するコードが、エラーを検出したコードに密に結合されている場合は、パフォーマンスが重要なループで代わりにエラー コードを使用するかどうかを検討します。
  • 例外をスローまたは伝達する可能性のある関数ごとに、strong 保証、basic 保証、nothrow (noexcept) 保証の 3 つの例外保証のいずれかを指定します。 詳細については、「方法: 例外安全性に対応した設計をする」をご覧ください。
  • 値渡しで例外をスローし、参照渡しでそれらの例外をキャッチします。 処理できない例外をキャッチしないでください。
  • C++11 で非推奨とされた例外指定を使用しないでください。 詳細については、「例外の指定と noexcept」セクションをご覧ください。
  • 標準ライブラリの例外の種類は、適用するときに使用します。 カスタム例外の種類は、exception例外クラス階層から取得します。
  • 例外がデストラクターまたはメモリ解放関数からエスケープしないようにしてください。

例外とパフォーマンス

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

例外とアサーション

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

C++ 例外と Windows SEH 例外

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

SEH の詳細については、「Structured Exception Handling (C/C++)」をご覧ください。

例外指定 と noexcept

例外指定は、関数がスローする可能性がある例外を指定する方法として C++ に導入されました。 ただし、実際には例外指定に問題があることがわかったため、C++11 ドラフト標準では非推奨とされます。 関数がどの例外のエスケープも許可しないことを示す throw() を除き、throw 例外指定を使用しないことをお勧めします。 非推奨形式 の throw( type-name ) の例外指定を使用する必要がある場合は MSVC サポートは制限されます。 詳細については、「例外指定 (スロー)」を参照してください。 noexcept 指定子は、throw() の推奨される代替手段として C++11 に導入されました。

関連項目

例外的なコードと非例外的なコードをインターフェイスで連結する方法
C++ 言語リファレンス
C++ 標準ライブラリ