本章では、Sun C++ コンパイラに現在実装されている例外処理、および C++ 国際規格の規定について説明します。
例外処理に関する追加情報については、『注解 C++ リファレンス・マニュアル』(Margaret A. Ellis、Bjarne Stroustrup 共著、トッパン刊) を参照してください。
例外とは、プログラムの通常の流れの中で発生し、プログラムの継続を阻止する変則性のことです。これらの変則性 (ユーザーエラー、論理エラーまたはシステムエラー) は、関数で検出できます。変則性を検出した関数がその変則性に対処できない場合は、例外を送出し、例外を処理する関数がそれを捕獲します。
C++ では、例外が送出されたときには、これを無視することはできません。つまり、何らかの通知をするか、プログラムを停止しなければなりません。ユーザーによって作成された例外ハンドラが存在しない場合は、プログラムはコンパイラのデフォルト機構によって強制終了します。
例外処理は、ループや if 文のような、プログラムの通常のフロー制御に比べると手間がかかります。そのため、例外機構は通常の動作の処理ではなく、実際に異常と認められる状況でのみ使用するようにしてください。
例外は、局所的に処理できない状況を処理する場合に特に便利です。プログラム全体にエラー状態を伝えるのではなく、エラーを処理できる場所へ直接制御を移すことができます。
たとえば、ファイルを開き、いくつかの関連データを初期化する処理が、ある関数に与えられているとします。ファイルが開けないか、あるいはファイルが破壊されている場合、この関数は処理を実行できません。このような問題を処理するための機能が関数に十分与えられていない場合、関数は問題を示す例外オブジェクトを送出し、プログラムの前方へ制御を移すことができます。例外ハンドラは、自動的にファイルのバックアップを行う、ユーザーに対して他のファイルで試すか尋ねる、プログラムを正常に停止する、などの処理を行えます。例外ハンドラの指定がないと、状態とデータを関数呼び出しの階層の全体に渡し、関数呼び出しごとに状態の検査を行う必要があります。例外ハンドラを指定する場合、エラー検査によって制御フローがわかりにくくなることはありません。関数が戻る場合、呼び出し元はその関数が正常に終了したと見なせます。
例外ハンドラにはいくつか欠点があります。関数またはその関数が呼び出す他の関数が例外を送出したため関数が戻らない場合には、データが矛盾した状態のままになることがあります。プログラマは、例外が送出される可能性があるのはいつか、例外がプログラムの状態に悪い影響を与えるかどうかを把握する必要があります。
C++ の例外ハンドラには次の 3 つのキーワードがあります。
try
catch
throw
try ブロックとは、例外が発生する可能性のある、通常中括弧 { } で囲まれた C++ 文の集まりです。このグループ化のため、例外ハンドラは try ブロック内で生成された例外だけを扱うことができます。各 try ブロックには、対応する catch ブロックが 1 つ以上存在します。
catch ブロックとは、特別に送出された例外を処理するために使用される C++ 文の集まりです。複数の catch ブロック (つまりハンドラ) が try ブロックの後に置かれます。catch ブロックは次の項目からなります。
キーワード catch
try ブロックから送出される可能性のある例外の型に対応した、括弧 ( ) に囲まれた catch パラメータ
例外を処理するための、中括弧 { } で囲まれた文の集まり
throw 文は、次の例外ハンドラに例外とその値を送出するために使用されます。通常のthrow ブロックは、キーワード throw と式から構成されます。式の結果の型によって、どの catch ブロックに制御が移るかが決まります。catch ブロック内では、現在の例外と値は throw キーワードだけ (式は不要) で再送出できます。
この例では、try ブロック中の関数呼び出しは f() に制御を渡します。f() は Overflow 型の例外を送出します。この例外は、Overflow 型の例外を処理する catch ブロックによって処理されます。
class Overflow { // ... public: Overflow(char,double,double); }; void f(double x) { // ... throw Overflow('+',x,3.45e107); } int main() { try { // ... f(1.2); //... } catch(Overflow& oo) { // Overflow 型の例外をここで処理する } }
例外ハンドラを実装する場合の基本作業を次に示します。
他の多くの関数から呼び出されている関数は、エラーが検出されるたびに例外が送出されるようコーディングしてください。通常、この throw 式はオブジェクトを 1 つ送出します。このオブジェクトは、例外の型を識別し、送出された例外に関する固有の情報を渡す際に使用します。
その関数を使用するプログラム中で try 文を使用して例外に備えてください。例外が発生すると思われる関数呼び出しを try ブロック内で中括弧で囲んでください。
try ブロックのすぐ後に、catch ブロックを 1 つ以上記述してください。 各 catch ブロックは、受け取ることのできるオブジェクトの型またはクラスを識別します。オブジェクトが例外によって送出されると、次のことが行われます。
例外により送出されるオブジェクトが catch 式の型と一致する場合は、この catch ブロックに制御が移ります。
例外により送出されるオブジェクトが先頭の catch ブロックと一致しない場合は、以降の catch ブロックから、型の一致するものが順次検索されます。
try ブロックが入れ子になっていて 1 つも一致するものがない場合は、最も内側にあるcatch ブロックから、try ブロックの外側にある一番近い catch ブロックに制御が移ります。
現在の関数内に一致する catch ブロックが見つからない場合は、現在の関数内の 「自動」 (局所的で非静的) オブジェクトはすべて破壊され、関数はただちに終了します。一致するcatch ブロックの検索は、現在の関数を呼び出した関数により継続して行われます。この処理は、関数 main に達するまで継続されます。
どの catch ブロックとも一致しない場合は、プログラムは事前定義済みの関数 terminate() を呼び出して正常終了します。terminate() はデフォルトで abort() を呼び出し、abort() は残っているオブジェクトをすべて破壊してプログラムを終了します。set_terminate() 関数を呼び出すと、このデフォルトの動作を変えることができます。
例外処理は、配列の範囲検査などの同期例外だけをサポートするように設計されています。同期例外という言葉は、例外が throw 式からのみ発生することを意味します。
C++ 標準では、終端モデルを使用した同期例外処理をサポートしています。終端とは、一度例外が送出されたらその場所には制御が戻らないことを意味します。
例外処理は、キーボード割り込みなどの非同期例外を直接処理するようには設計されていません。ただし、注意して行えば、非同期イベントの場合にも動作するように設定することもできます。たとえば、例外処理をシグナルと一緒に動作させるには、大域変数を設定し、この変数の値を定期的な間隔でポーリングして値が変化したときに例外を送出するようなシグナルハンドラを作成します。
C++ では、例外ハンドラは例外を訂正してその例外の発生場所に戻ることはしません。その代わり例外が発生すると、制御は例外を送出した関数から抜け、続いて例外を予期していた try ブロックから抜け、その例外と例外宣言が一致する catch ブロックに移ります。
この catch ブロックが例外を処理します。catch ブロックは、同じ例外を再送出するか、別の例外を送出するか、ラベルにジャンプするか、関数から戻るか、あるいは正常に終了します。catch ブロックが throw なしで正常に終了した場合、制御の流れは後続のすべての (try ブロックに関連付けられた) catch ブロックを飛び越えます。
例外が送出および捕獲され、その例外を送出した関数の外に制御が移ると、「スタックの巻き戻し」が実行されます。スタックの巻き戻しを行なっている間、終了したブロックのスコープ内で生成された自動オブジェクトは、そのデストラクタの呼び出しによって安全に破壊されます。
try ブロックが例外なしで終了した場合、関連するすべての catch ブロックは無視されます。
例外ハンドラは、 return 文を使用してエラーの発生した場所へ制御を戻すことはできません。この場合、発行された return 文はその catch ブロックが入っている関数から戻ります。
try ブロックやハンドラから外への分岐は許可されています。しかし、catch ブロックの中への分岐は、例外の開始を飛び越すことに等しいので許可されていません。
他の例外が処理されていない間に別の例外を送出することを、例外の入れ子と呼びます。例外の入れ子は、特定の状況でしか行えません。例外が送出される位置から一致する catch 節の入力位置までは、例外は処理されません。この間に呼び出される関数 (破壊される自動オブジェクトのデストラクタなど) は、例外が関数を回避しないかぎり、新しい例外を送出できます。他の例外が処理されていない間に例外によって関数が終了すると、その直後に terminate() 関数が呼び出されます。
例外ハンドラがいったん入力されると例外は処理済みと見なされ、例外が再び送出できるようになります。
例外が送出されているが、現在未処理である状態は uncaught_exception() 関数で確認できます。「uncaught_exception() 関数の呼び出し」を参照してください。
関数宣言には、例外指定を 1 つ含めることができます。例外指定とは、関数が直接的にまたは間接的に送出する可能性のある例外のことです。
次の 2 つの宣言は、関数 f1 は例外を生成し、その例外は X 型のハンドラが受け取ることおよび、型 W、Y、または Z のハンドラによって捕獲できる例外だけを関数 f2 が生成することを伝えています。
void f1(int) throw(X); void f2(int) throw(W,Y,Z);
void f3(int) throw(); // 空の括弧
このように定義すると、関数 f3 は例外を 1 つも生成しなくなります。例外指定で許可されていない例外によって関数が終了する場合、事前に定義済みの関数 unexpected() が呼び出されます。unexpected() は、デフォルトで abort() を呼び出してプログラムを終了します。set_unexpected() 関数を呼び出すと、このデフォルトの動作を変えることができます。「terminate() と unexpected() 関数の変更」を参照してください。
予期しない例外は、コンパイル時ではなくプログラムの実行時に検査されます。許可されていない例外が送出されそうな場合でも、実行時にその例外が実際に送出されないかぎりエラーは出力されません。
しかしコンパイラは、場合によっては不必要な検査を省くことができます。
void foo(int) throw(x); void f(int) throw(x); { foo(13); }
例外を指定しておかないと、あらゆる例外が送出される可能性があります。
例外に関連する実行時エラーメッセージには、次のものがあります。
例外処理のハンドラがありません
予期しない例外を送出
例ハンドラは例外の再送出しかできません
スタックの巻き戻し中は、デストラクタは独自の例外を処理しなければなりません
メモリーが足りません
実行時にエラーを検出すると、その例外の型と上記の 5 つのメッセージの 1 つがエラーメッセージとして表示されます。デフォルトでは、その後で事前定義済み関数の terminate() が呼び出されます。terminate() は abort() を呼び出します。
コンパイラは、コード生成を最適化する時に、例外指定で提供された情報を利用します。たとえば、例外を送出しない関数は最適化の対象から外されます。また、関数の例外指定に対する実行時検査はできる限り省略されます。このため、正しく例外が指定された関数を宣言することによって、コード生成の効率が向上します。
次に、set_terminate() と set_unexpected() を使用して terminate() 関数と unexpected() 関数の動きを変更する方法について説明します。
terminate() のデフォルトの動作は、次のように関数 set_terminate() を呼び出すことによって変更できます。
// 宣言は標準ヘッダー <exception> に含まれる namespace std { typedef void (*terminate_handler)(); terminate_handler set_terminate(terminate_handler f) throw(); void terminate(); }
terminate() 関数は、次のような場合に呼び出されます。
例外処理機構がユーザー関数 (自動オブジェクトのデストラクタを含む) を呼び出したがその関数が未捕獲の例外を残したまま、別の未捕獲の例外のために終了した
送出された例外のハンドラを例外処理機構が見つけられない
非局所的なオブジェクトが静的なオブジェクトとして存在している間に、その構築または破壊が、例外により終了した
atexit() で登録された関数の実行が例外により終了した
オペランドを持たない throw 式が例外を再送出しようとしたが、例外は現在処理されていない
unexpected()関数以前に違反のあった例外指定が許可しない例外を送出したが、std::bad_exception がその例外指定に含まれていない
unexpected() のデフォルト版が呼び出されている
terminate() は set_terminate() に引数として渡された関数を呼び出します。このような関数はパラメータを持たず、値を返すこともなく、プログラム (または現在のスレッド) を必ず停止します。set_terminate() への最後の呼び出しで渡された関数が呼び出されます。最後に呼び出された set_terminate() に引数として渡された以前の関数が戻り値になります。そのため terminate を使用し、今までに登録された関数を順次呼び出すようにプログラミングすることができます。terminate() のデフォルトの関数は、メインスレッドに対して abort() を呼び出し、他のスレッドに対して thr_exit() を呼び出します。thr_exit() はスタックを巻き戻したり、自動オブジェクトに対する C++ デストラクタを呼び出すことはありません。
呼び出し元に戻ったり、プログラムまたはスレッドを終了しないような関数を terminate() の代用として選択すると、エラーになります。
unexpected() のデフォルトの動作は、関数 set_unexpected() を呼び出すことによって変更できます。
// 宣言は標準ヘッダー <exception> に含まれる namespace std { class exception; class bad_exception; typedef void (*unexpected_handler)(); unexpected_handler set_unexpcted(unexpected_handler f) throw(); void unexpected(); }
unexpected() 関数は、関数がその例外指定にない例外によって終了しようとする場合に呼び出されます。unexpected() のデフォルト版は、terminate() を呼び出します。
ユーザーが変更した unexpected() は、例外指定が許可している例外も送出することがあります。このような場合の例外処理は、その例外が実際に元の関数から送出されたかのように継続します。変更後の unexpected() がそれ以外の例外を送出した場合は、その例外は標準の例外 std::bad_exception に置換されます。元の関数の例外指定が std::bad_exception を許可しない場合は、直後に関数 terminate() が呼び出されます。それ以外では、元の関数が実際に std::bad_exception を送出したかのように例外処理が継続します。
unexpected() は set_unexpected() に引数として渡された関数を呼び出します。このような関数は、パラメータを持たず、値を返すこともありません。このような関数はその呼び出し元に戻ってはなりません。set_unexpected() への最後の呼び出しで渡された関数が呼び出されるようになります。以前の set_unexpected() の呼び出し時に引数として渡された関数が戻り値になります。そのため set_unexpected() を使用し、今までに登録された関数を順次呼び出すようにプログラミングすることができます。
unexpected()の代わりに呼び出し元に戻る関数を選択するとエラーになります。
捕獲されていない (アクティブな) 例外とは、送出されたがハンドラにまだ受け付けられていない例外を意味します。関数 uncaught_exception() は、捕獲されていない例外が存在する場合は true を返し、存在しない場合は false を返します。
ある例外が捕獲されないために関数が終了したために、他の例外がまだアクティブな状態のままプログラムが停止してしまうことがあります。uncaught_exception() 関数は、このような問題を防ぐために役立ちます。この問題は、スタックの巻き戻しの間に呼び出されたデストラクタが例外を送出する時に最も多く発生します。対策としては、デストラクタ内で例外を送出する前に uncaught_exception() が false を返すように設定します (ほかの例外がアクティブな間にデストラクタが例外を送出するためにプログラムが停止することを防ぐには、デストラクタが例外を送出しないですむようにプログラムを設計することもできます)。
次のいずれかにあてはまる場合に、T 型のハンドラはE 型の throw と一致します。
T が E と同じ型である
T が、E の const か volatile である
E が、T の const か volatile である
T が E の参照か、E が T の参照である
T が E の公開基底クラスである
T と E の両方ともポインタ型で、かつ E は標準のポインタ変換を使用して T に変換 できる
参照やポインタ型の例外を送出すると、「ポインタのからまり」という問題が発生する可能性があります。これは、例外処理が完了する前にポインタの宛先または参照先のオブジェクトが破壊された場合に起こります。オブジェクトが送出される場合、コピーコンストラクタによりオブジェクトのコピーが必ず作成され、このコピーが catch ブロックに渡されます。そのため、局所的なオブジェクトまたは一時的なオブジェクトを送出しても安全です。
(X) 型と (X&) 型の両方のハンドラとも X 型の例外と一致しますが、意味は異なります。(X) 型のハンドラを使用すると、そのオブジェクトのコピーコンストラクタを (再び) 起動することになり、そのオブジェクトを切り捨てる可能性があります。ハンドラの型から派生した型のオブジェクトが送出される場合、オブジェクトは切り捨てられます。そのため、通常は参照によりクラスオブジェクトを捕獲する方が実行速度が速くなります。
try ブロックのハンドラは現われる順序で使用されます。派生クラスのハンドラを確実に起動するには、派生クラスのハンドラ (または派生クラスの参照へのポインタ) を基底クラスのハンドラより前に置いてください。
コンパイラは、例外のアクセス制御に関して次の検査を行います。
catch 節の仮引数が、catch 節のある関数の引数と同じ規則に従っているか
throw による送出の起きた関数のコンテキストでオブジェクトがコピーされたり、破壊されることがある場合、オブジェクトが送出されるか
現在では、アクセス制御は照合に影響を与えません。
「例外とハンドラの一致」に記載した照合規則以外、他のアクセス制御は実行時に検査されません。
クラス T の基底クラスまたはメンバーのコンストラクタが例外によって終了した場合、T コンストラクタがその例外を検出または処理する方法は通常ありません。例外は、T コンストラクタの本体に入る前 (つまり T 内の try ブロックに入る前) に送出されることになります。
C++ には、try ブロック内に関数全体を入れる新機能があります。通常の関数の場合、その効果は関数の本体を try ブロック内に入れることと変わりません。しかし、コンストラクタの場合、こうすることで try ブロックはコンストラクタのクラスの基底クラスとメンバーの初期設定子を回避する例外をすべてトラップするようになります。関数全体が try ブロックで囲まれる場合、そのブロックは「関数 try ブロック」と呼ばれます。
次の例では、基底クラス B またはメンバー E のコンストラクタから送出される例外はすべて T コンストラクタの本体に入る前に捕獲され、一致する catch ブロックにより処理されます。
catch ブロックは関数の外にあるため、関数 try ブロックのハンドラ内で return 文は使用できません。exit() を呼び出して例外を送出するか、あるいは terminate() を呼び出してプログラムを停止するしかありません。
class B { ... }; class E { ... }; class T : public B { public: T(); private: E e; }; T::T() try : B(args), E(args) { ... // コンストラクタの本体 } catch( X& x ) { ... // 例外 X を処理する } catch( ... ) { ... // 他の例外を処理する }
プログラム内で例外を使用しないことがわかっている場合は、コンパイラオプション -features=no%except を使用して、例外処理を行うためのコードが生成されないように設定できます。このオプションを使用すると、コードのサイズが幾分小さくてすむほか、コードの実行が速くなります。しかし、例外を無効にしてコンパイルされたファイルが例外を使用するファイルにリンクされる場合は、例外を無効にしてコンパイルされたファイル内の一部の局所的なオブジェクトは例外発生時に破壊されません。デフォルトでは、コンパイラは例外処理を行うためのコードを生成します。時間と容量のオーバーヘッドが重要でないかぎり、例外を有効にすることをお勧めします。
標準ヘッダー <exception> には、C++ 標準に規定されたクラスおよび例外に関連する関数が含まれています。このヘッダーにアクセスできるのは、標準モードで (コンパイラのデフォルトモード、あるいはオプション -compat=5 を使用して) コンパイルする場合だけです。次に、標準ヘッダーに含まれる宣言を示します。
// 標準ヘッダー <exception> namespace std { class exception { exception() throw(); exception(const exception&) throw(); exception& operator=(const exception&) throw(); virtual ‾exception() throw(); virtual const char* what() const throw(); }; class bad_exception: public exception { ... }; // 予期されない例外処理 typedef void (*unexpected_handler)(); unexpected_handler set_unexpected(unexpected_handler) throw(); void unexpected(); // 停止処理 typedef void (*terminate_handler)(); terminate_handler set_terminate(terminate_handler) throw(); void terminate(); bool uncaught_exception() throw(); }
標準クラス exception は、選択されている言語構造または C++ 標準ライブラリによって送出されるすべての例外の基底クラスです。 exception 型のオブジェクトについては、例外を生成することなく構築、コピー、破壊が可能です。仮想メンバー関数 what() は、例外を説明する文字列を返します。
C++ リリース 4.2 で使用される例外との互換性を保つため、標準モードで使用できるヘッダー <exception.h> も用意されています。このヘッダーファイルには、標準 C++ コードへの移行のために、標準の C++ の一部ではない宣言も含まれています。開発スケジュールが許す場合は、(<exception.h> ではなく <exception> を使用して) C++ 標準に準拠するようにコードを更新してください。
// 移行のために使用されるヘッダー <exception.h> #include <exception> #include <new> using std::exception; using std::bad_exception; using std::set_unexpected; using std::unexpected; using std::set_terminate; using std::terminate; typedef std::exception xmsg; typedef std::bad_exception xunexpected; typedef std::bad_alloc xalloc;
互換モード (オプション -compat=4) では、ヘッダー <exception> は使用できません。このモードでは、ヘッダー <exception.h> は C++ リリース 4.2 が提供するものと同じヘッダーを参照します。このヘッダーはここでは掲載していません。
共有ライブラリが dlopen によって開かれている場合は、RTLD_GLOBAL を使用して例外処理を実行する必要があります。
例外機能を使用して共有ライブラリを構築する場合、オプション -Bsymbolic を ld に渡さないでください。捕獲されるべき例外が見つからなくなる場合があります。
現在の例外処理実装はマルチスレッド対応になっています。あるスレッド内の例外は、別のスレッドの例外を妨害することはありません。しかし、例外をスレッド間でやりとりすることはできません。これは、あるスレッドが送出した例外を別のスレッドで受け取ることができないためです。
各スレッドごとに独自の terminate() 関数や unexpected() 関数を設定することができます。set_terminate() や set_unexpected() をあるスレッドで呼び出しても、その例外は呼び出し元のスレッドにだけ影響します。terminate() のデフォルトの関数は、メインスレッドであれば abort() で、その他のスレッドであれば thr_exit() です。「実行時のエラーの指定」を参照してください。
スレッドの取り消し (pthread_cancel(3T)) を行うと、スタック上の自動 (局所的で非静的) オブジェクトが破壊されます。スレッドが取り消されると、局所的デストラクタの実行の前に、ユーザーが pthread_cleanup_push() で登録した掃除用 (クリーンアップ) ルーチンが起動されます。特定のクリーンアップルーチンが登録された後に呼び出された関数の局所型オブジェクトは、ルーチンの実行前に破壊されます。