Effective C++Item 08:異常を解析関数から逃がさない

3613 ワード

1.C++は、解析関数から例外を放出することを禁じ得ないが、解析関数から例外を放出することは奨励されない
次のコードで分析します.
class Widget {
public:
	...
	~Widget(){...}    //             
};
void dosomething() {
	std::vectorv;      //v           
	...
}

上記のコードでは、関数dosomethingで宣言された変数vは、関数の実行が終了すると自動的に破棄されます.変数vectorは、含まれるすべてのWidgetsを破棄します.
v内に10個のWidgetが含まれていると仮定し、最初の要素を解析する際に異常が放出され、他の9つのWidgetsは破壊されるべきです(そうでなければ、彼らが保存しているどのリソースでもリソース漏洩が発生する)ため、vは他の9つの解析関数を呼び出すこともある.しかし、他の9つの解析関数が呼び出されると、2番目のWidgetの解析関数がまた異常を投げ出すと仮定すると、このとき2つの同時作用の異常があり、C++プログラムが実行を終了するのではなく、不明確な行為を招く.
標準ライブラリ内の他のコンテナまたはTR 1のいずれかのコンテナについても、上記のシーンでは同様の状況が発生します.したがって,C++は析出関数において異常を放出することを奨励しない.
2.解析関数で実行する動作に異常が出る可能性がある
上記の解析によれば、解析関数では例外を投げ出すべきではありませんが、あるシーンでは、解析関数が例外を投げ出すことは避けられません.
class DBConnection {
public:
	...
	static DBConnection create();  //      DBConnection  
	void close();                  //    ,       
};

上記のコードでは、クラスDBConnectionを使用してデータベース接続を担当し、DBConnectionオブジェクトでclose関数を呼び出すことを忘れないようにするために、DBConnectionリソースを管理するためのclassを作成し、その解析関数でclose関数を呼び出すのが合理的です.したがって、次のコードでクラスDBConn管理DBConnectionのリソースを作成します.
class DBConn {
public:
	...
	~DBConn() {
		db.close();            //        close  ,       
		                       //             DBConnection   
	}
private:
	DBConnection db;
};

クライアントには次のコードがあります.
{
	DBConn dbc(DBConnection::create());
}

上記のコードブロックでは、クラスDBConnectionのcreate関数を使用してDBConnectionオブジェクトを作成し、作成したオブジェクトをパラメータとしてDBConnectionオブジェクトdbcに渡し、リソースを管理します.
ブロック終了時にDBConnオブジェクトが破棄されるので、自動的にDBConnectionオブジェクトにclose関数を呼び出し、リソースを解放します.
構造関数で呼び出されたclose関数が成功した場合、問題はありません.しかし,この効用が異常を投げ出すと,DBConn構造関数がその異常を伝播し,上記のようなプログラムがクラッシュする「不明確な行為」の問題を引き起こす.
3.構造関数における異常な伝播を避ける
解析関数から放出される異常な伝播を回避するために、プログラムの「不明確な動作」の問題には、次の2つの解決策があります.
1)close関数が異常を投げ出すとプログラムを終了し、abort関数を呼び出すことで完了する:
DBConn::~DBConn() {
	try {
		db.close();
	}
	catch (...) {
		//      ,   close     
		std::abort();
	}
}

abort関数を用いて解析関数から異常が伝播するのを阻止すれば,不明確な挙動の発生を回避できる.
2)close呼び出しによる異常を飲み込む:
DBConn::~DBConn() {
	try {
		db.close();
	}
	catch (...) {
		//      ,   close     
	}
}

異常を飲み込むのは良い方法ではなく、「いくつかの動作が失敗した」という重要な情報を抑えている.しかし、異常を飲み込むのは「軽率なプログラム終了」や「不明確な行為によるリスク」よりも良い場合がある.
4.お客様に可能な異常に反応させる機会を与える
上記の2つの案はいずれも現れた異常に反応できない.お客様に発生した異常を処理する機会を与えるにはどうすればいいですか?
お客様が発生した異常を処理する機会を与えるために、DBConnインタフェースを再設計する必要があります.
class DBConn {
public:
	...
		void close() {
		db.close();
		closed = true;
	}
	~DBConn() {
		if (!closed) {         //          ,        close    
			try {
				db.close();           
			}
			catch (...) {
				             //      ,   close     

			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

上記のシナリオでは、
  • はDBConnクラスにユーザ呼び出し用のインタフェースcloseを提供し、ユーザに呼び出しによる異常を処理する機会を与える(ユーザ自身がcloseを呼び出すことはユーザの負担ではなく、ユーザにエラーを処理する機会を与える);
  • 属性closedは、リソースのクローズが成功したかどうか、DBConnが管理するDBConnectionがクローズされたかどうかを追跡するために使用され、クローズされていない場合は、close関数を呼び出して接続をクローズします.
  • しかし、構造関数で呼び出されたclose関数が異常を投げ出すと、「プログラムの終了を強制する」または「異常を飲み込む」という苦境に直面します.

  • 5.まとめ
    1.構造関数は絶対に異常を投げ出さないでください.解析関数が例外を投げ出すと、「プログラムの早期終了」または「不明確な動作の発生」のリスクが発生します.リスクを回避するために、構造関数で呼び出された関数が例外を放出する可能性がある場合は、構造関数は例外をキャプチャし、例外または終了プログラムを飲み込む必要があります.
    2.クライアントが操作関数の実行中に放出された異常に反応する必要がある場合、classは、構造関数ではなく一般的な関数を提供します.