2005-07-25
例外処理機構は、ある意味でCとC++の間で最も大きな差異を作り出している要因であるといえます。例外処理機構は非常に強力な仕組みですが、C++のプログラムサイズが増大する最大の要因にもなっています。
例外処理のためのランタイムには、std::terminate関数などの「目に見える」部分とtry/catch/throwといった基本構造を実現するための「目に見えない」部分に分かれます。特に、この「目に見えない」部分は、処理系によって実装方法が大きく異なります。
まずは比較的簡単な「目に見える」部分について解説します。目に見える部分はライブラリ関数として実装されており、例外処理が特定の状態になったときに呼び出されるハンドラ、例外処理の状態を調べる関数、および例外クラスからなります。
std::terminateは、送出された例外が最後までcatchされなかった場合や、例外がcatchされる前に別の例外が発生した(二重例外)場合に、呼び出される関数で、実際の動作はstd::set_terminateで設定することができます。
std::unexpectedは、例外指定(関数の宣言定義でthrow(型...)として、送出される可能性を列挙した記述)と矛盾した例外が送出された場合に呼び出されるハンドラで、実際の動作はstd::set_unexpectedで設定することができますが、いずれにしてもプログラムは強制終了します。
void foo() throw(std::logic_error)
{
throw std::runtime_error("test"); // 例外指定と矛盾した例外を送出
}
std::unexpectedのデフォルトの動作では、例外指定にstd::bad_exceptionが含まれている場合、実際に送出された例外をstd::bad_exceptionに置き換えます。そうでなければstd::terminateを呼び出し、プログラムを強制終了させます。
std::uncaught_exceptionは、例外が送出されてからcatchされるまでの間であればtrueを、それ以外ではfalseを返します。これによって、例外発生時だけに特定の処理を呼び出したり、二重例外を回避することができます。
これらの関数やクラスは
ヘッダで宣言定義されています。詳細な定義はヘッダを見るなどしてください。
次に「目に見えない」部分のランタイムについて解説します。前述したように、この部分の実現方法は処理系によって大きく異なっています。代表的な実現方法では、内部でsetjmp/longjmpを呼び出しているようです。
この部分のランタイムが、単なるsetjmp/longjmpと大きく異なるのは、例外がthrowされてからcatchされるまでの間にある、自動オブジェクトのデストラクタを呼び出す点です。実際にどれだけのデストラクタが呼び出されるかは、例外が送出された時点のブロックに依存します。
どれだけのデストラクタを呼び出せばよいかの情報は、例えばリスト構造を用いるなどして、ブロックに出入りするたびに再設定しなければなりません。*1これはコンテキストに強く依存するため、マルチタスク環境では、タスクごとに個別に管理する必要が発生します。
もうひとつ、setjmp/longjmpと異なる点は、longjmpが整数値しか返せなかったのに対して、例外処理では型情報を持ったオブジェクトを送出できることです。オブジェクトを送出するためには、コピーするための領域を動的に割り付ける必要が発生します。
例外が送出されるとき、たとえ参照としてcatchされる場合であっても、オブジェクトのコピーは必ず発生します。throwする時点ではどんな方法でcatchされるか分からないのと、スタックを巻き戻すことで寿命が尽きてしまうオブジェクトを参照させるわけにはいかないからです。
try
{
throw std::logic_error("test");
// もしコピーされなければ、この時点で例外オブジェクトの寿命は尽きる
}
catch (std::logic_error& e)
{
}
オブジェクトのコピー先の領域は、スタック以外から動的に割り付ける必要があります。newで割り付けてもよいのですが、new式自体が例外を送出する場合でも正しく動作させるためには、非常用の静的な予備領域をいくらか確保しておく必要があります。
当然のことながら、動的に割り付けたコピー用の領域は、使い終わった時点で解放しなければなりませんし、解放の前には例外オブジェクト自身のデストラクタを呼び出す必要もあります。
このように、例外処理のためには、かなり複雑な処理を目に見えない裏側で行わなければなりません。C++を使うとプログラムサイズが大きくなるのは、例外が送出されたときにどのデストラクタを呼び出すべきかの管理情報が生成されることによる影響が最も大きいといえます。
*1 この制御に代わる処理を、実際に例外が送出される時点で行う処理系もあります。そうした処理系では、例外が発生しない限り、実行速度の面では有利ですが、ひとたび例外が発生すれば、非常に長い処理時間がかかります。
Comment