[C++]異常処理


異常は非正常な行為を処理できるメカニズムであり、異常発生端と異常処理端との通信の一種でもある.ソフトウェア開発では、通常、一方がどのような異常が発生するかを知っているが、どのように処理するか分からないが、他方が異常が発生するかどうか分からないが、異常が発生した場合に処理することができる.したがって,C++は異常処理機構を提供する.ライブラリの開発者が異常が発生したときに異常を放出し、ライブラリのユーザーが異常をキャプチャし、合理的な処理を行うことを許可します.
C++には、例外処理に関するキーワードがあります.
throw:異常を投げ出す.
try:プログラムブロックが異常を放出するかどうかをテストします.
catch:異常をスナップし、処理します.
したがって、一般的な例外ハンドラのフレームワークは、
ライブラリの開発者:
void throw_test()
{
	if(something_is_bad) {
		throw exception;
	}
}

ライブラリのユーザー側:
void catch_test()
{
	try {
		throw_test();
	} catch(exception & e) {
		// exception handler
	}
}

上記のようにthrow_test()にexception異常が投げ出されcatch_test()でthrow_test()はテストを行い,異常が発生するとcatch文にスナップされ,処理を行う.
1スナップ端で参照を使用するかどうか
読者は上記の異常捕捉を見たときに引用を使用していますが、必ず引用を使用しますか?
一般的にパラメータを渡す方法は、ポインタの伝達、値の伝達、参照の伝達の3つです.実際には、転送ポインタと転送値は似ています.
異常をキャプチャする場合、ポインタ伝達を使用して異常を放出すると、現在の役割ドメインから離れます.そのため、ポインタが指すオブジェクトが依然として存在することを保証する必要があります.staticオブジェクト、グローバルオブジェクト、スタック上のオブジェクトの3つの方法で保証できます.staticとグローバル変数を使用する場合、これは通常プログラマーの習慣に合わない.例外を渡すたびにstaticとグローバル変数を宣言しなければならないため、これはもちろん良いプログラミングスタイルではない.スタック上のオブジェクトを用いて放出端でnewを行う場合、deleteはいつ行われますか?ユーザは、最後に処理した後にオブジェクトをdeleteすることを保証しなければならない.これはプログラマーに大きな負担をもたらす.
異常をキャプチャする場合、値伝達を使用する場合は、2回のコピーを生成する必要があり、マルチステート動作はありません.具体的には第2部をご覧ください.
したがって、オブジェクトを放出するときは、できるだけリファレンス伝達を使用する必要があります.リファレンス伝達を使用すると、上記の問題はありません.
2 throwの後に何があったの?
リファレンス伝達を使用します.通常、放出端には次の2つの形式があります.
derived d;
throw d;

throw derived();

1つ目は局所オブジェクトを用い,2つ目は一時オブジェクトを用いる.では、現在の役割ドメインを離れるとdが解析されますが、dのオブジェクトがスナップ側に伝達されることを保証するとしますか?
throwオブジェクトが1つある場合、コンパイラはスタック上に投げ出されたオブジェクトをコピーして一時的なオブジェクト(G++の実装)を構築するので、現在の役割ドメインを離れてもスタック上に同じオブジェクトが投げ出されたオブジェクトのコピーになります.この場合、スナップ側によって動作が異なります.
伝達ポインタは異常処理において通常不慣れであるため,ここでは伝達ポインタについては議論しない.
スナップ側がcatch by valueで例外をスナップしている場合:
void catch_test()
{
	try {
		throw_test();
	} catch(exception e) {
		// exception handler
	}
}

コピーコンストラクション関数を呼び出してeを構築します.したがって、例外を値でスナップすると、通常、コピーコンストラクション関数の呼び出しが2回行われます.
スナップ側がcatch by referenceでオブジェクトをスナップしている場合:
void catch_test()
{
	try {
		throw_test();
	} catch(exception& e) {
		// exception handler
	}
}

eはそのスタックの一時オブジェクトにバインドされます.したがって、リファレンス伝達は、コンストラクション関数の呼び出しを1回だけコピーし、ベースクラスのリファレンスを派生クラスのオブジェクトにバインドし、マルチステート動作を使用することもできます.
一方、値で例外をスナップすると、派生クラスのオブジェクトが放出され、ベースクラスのオブジェクトがスナップされます.この場合、オブジェクトの切断が発生し、マルチステート動作はありません.(オブジェクトのパラメータ伝達と同様)
これは、第1のセクションでスナップ例外を参照することが望ましいことも検証した.
3異常説明
異常説明は、1つの関数が投げ出す異常のタイプを説明する方法であり、その関数がどのタイプを投げ出すかを示すことができ、処理端でどの異常を処理する必要があるかを予測することができる.
例外の説明には、通常3つの形式があります.
void func() throw (); //     

void func() throw (type1, type2); //     type1 type2   

void func(); //         ,                   void func() throw (...);   g++     

したがって,関数を記述する際に,ある関数があるタイプの異常を投げ出すと判断した場合,その関数が投げ出す異常タイプを説明するために異常説明を加えることができる.
もちろん、すべてが正常に動作していれば、関数が何らかのタイプの異常を投げ出すと、呼び出し側はどのような異常を処理しますが、関数が投げ出す異常が関数の異常説明と一致しなければどうなりますか.
関数が放出されるタイプが例外の説明と一致しない場合、terminate()を呼び出す特殊な関数unexpected()が呼び出されます.terminate()のデフォルトの動作はabort()です.つまり、関数が自分の例外の説明に違反した場合、例外の説明と一致しない例外を放出すると、プログラムは直接終了します.
しかし、異常説明は通常、プログラマに関数がどのような異常を投げ出すか、プログラマが呼び出し側でどのような異常を処理する必要があるかを示すヒント作用であり、関数が異常説明と一致しない異常を投げ出すことが多い場合、異常説明は役に立たない.それはプログラマを提示する役割を果たすことができないからである.
次の例を示します.
void catch_test(int x) throw (int)
{
	if(x == 0) {
		throw 3.0f;
	}
        throw 0;
}

関数の異常説明はintですが、関数体にはfloatタイプの異常が投げ出されています.異常は通常タイプ変換できないので、プログラムがcatch_を呼び出すとtest()では、プログラムが異常に終了します.
解決策は3つあります.
(1)異常説明を修正する.
(2)set_を呼び出すunexpected()修正関数異常と異常説明が一致しない場合のコールバック関数を放出し、一致しない異常を統一した異常に修正します.
class unexpected_exception {
};

void convert_unexpected()
{
	cout << "throw unexpected exception" << endl;
	throw unexpected_exception();
}

void catch_test(int x) throw (int, unexpected_exception)
{
	if(x == 0) {
		throw 3.0f;
	}
}

int main()
{
	set_unexpected(convert_unexpected);
	
	try {
		catch_test(0);
	} catch(int x) {
		cout << x << endl;
	} catch(unexpected_exception &x) {
		cout << "catch unexpected exception" << endl;
	}
}

上のコードに示すようにmain関数の開始でset_を呼び出しますunexpected()コールバック関数をconvert_に設定unexpected()は、すなわち、関数が投げ出す異常が異常説明タイプと一致しない場合にconvert_を呼び出すunexpected()でconvert_unexpected()関数でunexpected_を放出Exception例外です.したがって、例外の説明と一致しない例外はunexpected_を再放出します.Exceptionタイプの異常.
メイン関数でcatch_をtest()はcatch_test()のパラメータが0の場合floatタイプの異常が投げ出され、異常説明のintと一致しないためcatch_が呼び出されるtest(0)の場合convert_が呼び出されますunexpected()関数は、unexpected_を再放出します.Exception異常は、catch時に処理できます.
ここで注意したいのは、関数の例外宣言にunexpected_を追加することです.Exceptionタイプなので、ここではunexpected()を置き換えて変換しただけです.
(3)unexpected関数を置換した関数でこの異常を再放出し、この異常が標準異常bad_として扱われるException再放出.
class unexpected_exception {
};

void convert_unexpected()
{
	cout << "throw unexpected exception" << endl;
	throw;
}

void catch_test(int x) throw (int, bad_exception)
{
	if(x == 0) {
		throw 3.0f;
	}
}

int main()
{
	set_unexpected(convert_unexpected);
	
	try {
		catch_test(0);
	} catch(int x) {
		cout << x << endl;
	} catch(bad_exception &x) {
		cout << "catch unexpected exception" << endl;
	}
}

上記のコードに示すように、コールバック関数convert_unexpected()でthrowを使用します.異常を再放出するとbad_に変換されますException標準異常再放出.
4異常について注意すべき問題:
(1)大規模なプロジェクトでは、関数の呼び出し階層が深く、プログラマは関数がどのような異常を投げ出すか予測できません.なぜなら、呼び出された関数が投げ出した異常もそれ自身に転嫁されるからです.この場合、unexpected()を呼び出す確率が大幅に増加し、1つの関数が異常を投げ出さないと思っているが、呼び出された関数が異常を投げ出すと、3つの不一致の問題が発生する.
(2)異常説明をテンプレートと組み合わせるのは良い考えではない.テンプレートの挙動が変化し,さらに予想できないからである.
(3)異常を使用するには異常のコストも考慮し,try文を用いて異常のテストを行い,tryを離れると一部のオブジェクトが破棄されるため,プログラムはtry文の開始と終了の位置を覚えて合理的な処理を行う必要がある.throwを使用して例外を放出すると、スタックに一時的な例外オブジェクトが作成され、コンパイラが破棄します.これはいずれも異常処理に考慮すべきコストである.したがって,プログラムが異常を投げ出さないと判断した場合には,決して異常処理を用いず,やむを得ない場合にのみ異常処理を用いる.
参考資料:
More Effective C++第三章:異常