方法: 例外安全性に対応した設計

例外機構の利点の 1 つは、例外をスローするステートメントから例外を処理する最初の catch ステートメントに、例外に関するデータと共に実行が直接ジャンプすることです。 ハンドラーは呼び出し履歴の何レベル上であってもかまいません。 try ステートメントと throw ステートメントの間で呼び出された関数は、スローされる例外に関して何も知る必要がありません。 ただし、例外が下から上に通知される可能性があるどの時点でも、予期せずにスコープから外れることができるように関数が設計されている必要があり、部分的に作成されたオブジェクト、リークしたメモリ、使用不能な状態のデータ構造体などが部分的に残らないようになっている必要があります。

基本技術

堅牢な例外処理ポリシーには、熟考が必要であり、デザイン プロセスの一部になっている必要があります。 一般に、ほとんどの例外は、ソフトウェア モジュールの下位層で検出されてスローされますが、一般にこれらの層には、エラーを処理したり、エンド ユーザーにメッセージを通知するための十分なコンテキストがありません。 中間層の関数は、例外オブジェクトを検査する必要があったり、最終的に例外をキャッチする上位層のための有用な追加情報がある場合に、例外をキャッチして再スローできます。 関数が例外をキャッチしてそれを "飲み込む" ことができるのは、例外から完全に回復できる場合のみです。 多くの場合、中間層の正しい動作は、呼び出し履歴の上方向へ例外を伝達することです。 最上位層でも、例外によってプログラムがその正しさを保証できない状態になる場合は、例外を処理せずにプログラムを終了するのが適切な場合があります。

関数が例外を処理する方法にかかわらず、"例外セーフ" であることを保証するには、次の基本的なルールに従って関数が設計されている必要があります。

リソース クラスを単純にする

クラスに手動リソース管理をカプセル化する場合は、単一のリソースを管理する以外に何も実行しないクラスを使用します。 クラスを単純なままにすると、リソース リークが発生するリスクが軽減されます。 次の例に示すように、できるだけスマート ポインターを使用します。 この例は、shared_ptr を使用した場合の相違点を強調するため、意図的に作成し簡略化されています。

// old-style new/delete version
class NDResourceClass {
private:
    int*   m_p;
    float* m_q;
public:
    NDResourceClass() : m_p(0), m_q(0) {
        m_p = new int;
        m_q = new float;
    }

    ~NDResourceClass() {
        delete m_p;
        delete m_q;
    }
    // Potential leak! When a constructor emits an exception,
    // the destructor will not be invoked.
};

// shared_ptr version
#include <memory>

using namespace std;

class SPResourceClass {
private:
    shared_ptr<int> m_p;
    shared_ptr<float> m_q;
public:
    SPResourceClass() : m_p(new int), m_q(new float) { }
    // Implicitly defined dtor is OK for these members,
    // shared_ptr will clean up and avoid leaks regardless.
};

// A more powerful case for shared_ptr

class Shape {
    // ...
};

class Circle : public Shape {
    // ...
};

class Triangle : public Shape {
    // ...
};

class SPShapeResourceClass {
private:
    shared_ptr<Shape> m_p;
    shared_ptr<Shape> m_q;
public:
    SPShapeResourceClass() : m_p(new Circle), m_q(new Triangle) { }
};

RAII の表現形式を使用してリソースを管理する

関数を例外セーフにするには、例外がスローされた場合でも、mallocnew を使用して割り当てたオブジェクトが破棄され、ファイル ハンドルなどのすべてのリソースが閉じられるか解放されるようにする必要があります。 Resource Acquisition Is Initialization (RAII) の表現形式は、そのようなリソースの管理を、自動変数の有効期間に結び付けます。 正常に返るか例外により関数がスコープから外れた場合、すべての完全構築された自動変数のデストラクターが呼び出されます。 スマート ポインターなどの RAII ラッパー オブジェクトは、デストラクターの中で適切な delete または close 関数を呼び出します。 例外セーフなコードでは、なんらかの種類の RAII オブジェクトに各リソースの所有権をすぐに渡すことが非常に重要です。 vectorstringmake_sharedfstream および類似クラスは、リソースの取得を処理することに注意してください。 しかし、unique_ptr と従来の shared_ptr の構造は、リソースの取得がオブジェクトではなくユーザーによって行われるという点で特殊です。したがって、Resource Release Is Destruction と見なされ、RAII としては疑問の余地があります。

3 種類の例外保証

一般に、例外の安全性は、関数が提供可能な 3 種類の例外保証に関して議論されます。それは、no-fail 保証strong 保証basic 保証です。

No-fail 保証

no-fail (または no-throw) 保証は、関数が提供できる最も強力な保証です。 それは、関数が例外をスローしないか、例外の伝達を許可することを示します。 ただし、(a) この関数が呼び出すすべての関数が no-fail であることがわかっているか、(b) スローされるすべての例外がこの関数に達する前にキャッチされることがわかっているか、(c) この関数に達する可能性があるすべての例外をキャッチし正しく処理する方法がわかっている場合を除いて、確実にこのようなことを保証できません。

strong 保証と basic 保証のどちらも、デストラクターが no-fail であることを前提としています。 標準ライブラリのすべてのコンテナーと型は、デストラクターが例外をスローしないことを保証します。 また、逆の要件もあります。標準ライブラリでは、テンプレート引数として渡される型など、ユーザー定義型のデストラクターが、例外をスローしないことが必要です。

Strong 保証

strong 保証は、関数が例外によりスコープから外れる場合、メモリをリークせず、プログラムの状態が変化しないことを示します。 strong 保証を提供する関数は、基本的にコミットまたはロールバック セマンティクスを持つトランザクションです。つまり、完全に成功するか無効かのどちらかになります。

Basic 保証

basic 保証は、3 つのうちで最も弱い保証です。 ただし、strong 保証がメモリ消費またはパフォーマンスの点で高価すぎる場合に最善の選択肢となる場合があります。 basic 保証は、例外が発生した場合は、データが変更された可能性があるとしても、メモリがリークせず、オブジェクトが引き続き使用可能な状態にあることを示します。

例外セーフなクラス

クラスは、安全でない関数で使用される場合であっても、自身が部分的に構築されたり部分的に破棄されるのを防ぐことにより、自身の例外セーフ性を保証するのに役立つことがあります。 クラス コンストラクターが完了前に終了した場合、オブジェクトは作成されず、そのデストラクターは決して呼び出されません。 例外の前に初期化された自動変数のデストラクターは呼び出されますが、動的に割り当てられたメモリや、スマート ポインターなどの自動変数によって管理されていないリソースはリークすることになります。

組み込みの型はすべて no-fail であり、標準ライブラリの型は少なくとも basic 保証をサポートしています。 例外セーフである必要があるすべてのユーザー定義型について、次のガイドラインに従ってください。

  • スマート ポインターまたは他の RAII 型ラッパーを使用してすべてのリソースを管理します。 コンストラクターが例外をスローするとデストラクターが起動されないので、クラスのデストラクターにリソース管理機能を持たせないようにします。 ただし、クラスが 1 種類のリソースのみを制御する専用のリソース マネージャーである場合は、デストラクターを使用してリソースを管理してもかまいません。

  • 基底クラスのコンストラクターでスローされる例外は、派生クラスのコンストラクターで飲み込むことができない点に注意します。 基底クラスの例外を派生クラスのコンストラクターで変換して再スローする場合は、関数の try ブロックを使用します。

  • すべてのクラスの状態を、スマート ポインターでラップされたデータ メンバーに格納するかどうかを検討します。特に、クラスに "初期化が許可されている" 初期化の概念がある場合に発生します。C++ では初期化されていないデータ メンバーを使用できますが、初期化されていない、または部分的に初期化されたクラス インスタンスはサポートしません。 コンストラクターは成功または失敗のいずれかであることが必要です。コンストラクターが最後まで実行されない場合、オブジェクトは作成されません。

  • デストラクターから例外が漏れないようにします。 C++ の基本的な原則は、デストラクターで例外が呼び出し履歴の上方向へ伝達されないようにすることです。 デストラクターで例外をスローする可能性がある操作を実行する必要がある場合は、try catch ブロックで実行し、例外を飲み込む必要があります。 標準ライブラリでは、定義されているすべてのデストラクターでこのことが保証されます。

関連項目

例外とエラー処理に関する最新の C++ のベスト プラクティス
方法: 例外的なコードと非例外的なコードをインターフェイスで連結する