| C++ プログラミングガイド |
第 5 章
例外処理
本章では、Sun C++ コンパイラに現在実装されている例外処理、および C++ 国際規格の規定について説明します。
例外処理に関する追加情報については、『注解 C++ リファレンス・マニュアル』(Margaret A. Ellis、Bjarne Stroustrup 共著、トッパン刊) を参照してください。
例外処理とは
例外とは、プログラムの通常の流れの中で発生し、プログラムの継続を阻止する変則性のことです。これらの変則性 (ユーザーエラー、論理エラーまたはシステムエラー) は、関数で検出できます。変則性を検出した関数がその変則性に対処できない場合は、例外を送出し、例外を処理する関数がそれを捕獲します。
C++ では、例外が送出されたときには、これを無視することはできません。つまり、何らかの通知をするか、プログラムを停止しなければなりません。ユーザーによって作成された例外ハンドラが存在しない場合は、プログラムはコンパイラのデフォルト機構によって強制終了します。
例外処理は、ループや
if文のような、プログラムの通常のフロー制御に比べると手間がかかります。そのため、例外機構は通常の動作の処理ではなく、実際に異常と認められる状況でのみ使用するようにしてください。例外は、局所的に処理できない状況を処理する場合に特に便利です。プログラム全体にエラー状態を伝えるのではなく、エラーを処理できる場所へ直接制御を移すことができます。
たとえば、ファイルを開き、いくつかの関連データを初期化する処理が、ある関数に与えられているとします。ファイルが開けないか壊れている場合、この関数は処理を実行できません。このような問題を処理するための機能が関数に十分与えられていない場合、関数は問題を示す例外オブジェクトを送出し、プログラムの前方へ制御を移すことができます。例外ハンドラは、自動的にファイルのバックアップを行う、ユーザーに対して他のファイルで試すか尋ねる、プログラムを正常に停止する、などの処理を行えます。例外ハンドラの指定がないと、状態とデータを関数呼び出しの階層の全体に渡し、関数呼び出しごとに状態の検査を行う必要があります。例外ハンドラを指定する場合、エラー検査によって制御フローがわかりにくくなることはありません。関数が戻る場合、呼び出し元はその関数が正常に終了したと見なせます。
例外ハンドラにはいくつか欠点があります。関数またはその関数が呼び出す他の関数が例外を送出したため関数が戻らない場合には、データが矛盾した状態のままになることがあります。プログラマは、例外が送出される可能性があるのはいつか、例外がプログラムの状態に悪い影響を与えるかどうかを把握する必要があります。
マルチスレッド環境で例外を使用する方法については、「マルチスレッドプログラムでの例外の使用」を参照してください。
例外処理キーワードの使用
C++ の例外ハンドラには次の 3 つのキーワードがあります。
try
tryブロックとは、例外が発生する可能性のある、中括弧{ }で囲まれた C++ 文の集まりです。このグループ化のため、例外ハンドラはtryブロック内で生成された例外だけを扱うことができます。各tryブロックには、対応するcatchブロックが 1 つ以上存在します。
catch
catchブロックとは、特別に送出された例外を処理するために使用される C++ 文の集まりです。複数のcatchブロック (つまりハンドラ) がtryブロックの後に置かれます。catchブロックは次の項目からなります。
throw
throw文は、次の例外ハンドラに例外とその値を送出するために使用されます。通常のthrowブロックは、キーワードthrowと式から構成されます。式の結果の型によって、どのcatchブロックに制御が移るかが決まります。catchブロック内では、現在の例外と値はthrowキーワードだけ (式は不要) で再送出できます。この例では、
tryブロック中の関数呼び出しはf()に制御を渡します。f()はOverflow型の例外を送出します。この例外は、Overflow型の例外を処理するcatchブロックによって処理されます。
例外ハンドラの実装
- 他の多くの関数から呼び出されている関数は、エラーが検出されるたびに例外が送出されるようコーディングしてください。通常、この
throw式はオブジェクトを 1 つ送出します。このオブジェクトは、例外の型を識別し、送出された例外に関する固有の情報を渡す際に使用します。- その関数を使用するプログラム中で
try文を使用して例外に備えてください。例外が発生すると思われる関数呼び出しをtryブロック内で中括弧で囲んでください。tryブロックのすぐ後に、catchブロックを 1 つ以上記述してください。
各catchブロックは、受け取ることのできるオブジェクトの型またはクラスを識別します。オブジェクトが例外によって送出されると、次のことが行われます。
- 例外により送出されるオブジェクトが
catch式の型と一致する場合は、このcatchブロックに制御が移ります。- 例外により送出されるオブジェクトが先頭の
catchブロックと一致しない場合は、以降のcatchブロックから、型の一致するものが順次検索されます。「例外とハンドラの一致」を参照してください。- 現在のスコープに送出された例外に対応する
catchブロックが存在しない場合、制御は現在のスコープ外に移り、そのスコープ内に定義されている自動 (局所的な非静的) オブジェクトはすべて破棄されます。次に、(関数のスコープである可能性がある) 周囲のスコープが対応するハンドラを持っているかどうかが検査されます。この処理は、対応するcatchブロックを持つスコープが見つかるまで続けられます。対応するcatchブロックが関数mainに到達するまでに見つかった場合、そのcatchブロックに制御が移ります。- 対応する
catchブロックがない場合は、プログラムは事前定義済みの関数terminate()を呼び出して正常終了します。terminate()はデフォルトでabort()を呼び出し、abort()は残っているオブジェクトをすべて破棄してプログラムを終了します。set_terminate()関数を呼び出すと、このデフォルトの動作を変えることができます。同期例外処理
例外処理は、配列の範囲検査などの同期例外だけをサポートするように設計されています。同期例外という言葉は、例外が
throw式からのみ発生することを意味します。C++ 標準では、終端モデルを使用した同期例外処理をサポートしています。終端とは、一度例外が送出されたらその場所には制御が戻らないことを意味します。
非同期例外処理
例外処理は、キーボード割り込みなどの非同期例外を直接処理するようには設計されていません。ただし、注意して行えば、非同期イベントの場合にも動作するように設定することもできます。たとえば、例外処理をシグナルと一緒に動作させるには、大域変数を設定し、この変数の値を定期的な間隔でポーリングして値が変化したときに例外を送出するようなシグナルハンドラを作成します。シグナルハンドラからは例外を送出できません。
制御の流れの管理
C++ では、例外ハンドラは例外を訂正してその例外の発生場所に戻ることはしません。その代わり例外が発生すると、制御は例外を送出した関数から抜け、続いて例外を予期していた
tryブロックから抜け、その例外と例外宣言が一致するcatchブロックに移ります。この
catchブロックが例外を処理します。catchブロックは、同じ例外を再送出するか、別の例外を送出するか、ラベルにジャンプするか、関数から戻るか、あるいは正常に終了します。catchブロックがthrowなしで正常に終了した場合、制御の流れは後続のすべての (tryブロックに関連付けられた)catchブロックを飛び越えます。例外が送出および捕獲され、その例外を送出した関数の外に制御が移ると、「スタックの巻き戻し」が実行されます。スタックの巻き戻しを行なっている間、終了したブロックのスコープ内で生成された自動オブジェクトは、そのデストラクタの呼び出しによって安全に破棄されます。
tryブロックが例外なしで終了した場合、関連するすべてのcatchブロックは無視されます。
注 - 例外ハンドラは、return文を使用してエラーの発生した場所へ制御を戻すことはできません。この場合、発行されたreturn文はそのcatchブロックが入っている関数から戻ります。
tryブロックとハンドラからの分岐
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()は、プログラムを終了させるterminate()を呼び出します。このデフォルト動作はset_unexpected()関数を呼び出すことで変更できます。「terminate() と unexpected() 関数の変更」を参照してください。予期しない例外は、コンパイル時ではなくプログラムの実行時に検査されます。許可されていない例外が送出されそうな場合でも、実行時にその例外が実際に送出されないかぎりエラーは出力されません。
しかしコンパイラは、場合によっては不必要な検査を省くことができます。
void foo(int) throw(x);void f(int) throw(x);{ foo(13);}例外を指定しておかないと、あらゆる例外が送出される可能性があります。
実行時のエラーの指定
例外に関連する実行時エラーメッセージには、次のものがあります。
実行時にエラーを検出すると、その例外の型と上記の 5 つのメッセージの 1 つがエラーメッセージとして表示されます。デフォルトでは、その後で事前定義済み関数の
terminate()が呼び出されます。terminate()はabort()を呼び出します。コンパイラは、コード生成を最適化する時に、例外指定で提供された情報を利用します。たとえば、例外を送出しない関数は最適化の対象から外されます。また、関数の例外指定に対する実行時検査はできる限り省略されます。このため、正しく例外が指定された関数を宣言することによって、コード生成の効率が向上します。
terminate()とunexpected()関数の変更次に、
set_terminate()とset_unexpected()を使用してterminate()関数とunexpected()関数の動きを変更する方法について説明します。マルチスレッド環境で例外を使用する方法については、「マルチスレッドプログラムでの例外の使用」を参照してください。
set_terminate()
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()以外の方法を使用する場合、呼び出し元に戻ってはなりません。
set_unexpected()
unexpected()のデフォルトの動作は、関数set_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()関数の呼び出し捕獲されていない (アクティブな) 例外とは、送出されたままハンドラにまだ受け付けられていない例外を意味します。関数
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による送出の起きた関数のコンテキストでオブジェクトがコピーされたり
破棄されることがある場合、オブジェクトが送出されるか「例外とハンドラの一致」に記載した照合規則以外、他のアクセス制御は実行時に検査されません。
tryブロック内に関数を入れるクラス
Tの基底クラスまたはメンバーのコンストラクタが例外によって終了した場合、Tコンストラクタがその例外を検出または処理する方法は通常ありません。例外は、Tコンストラクタの本体に入る前 (つまりT内のtryブロックに入る前) に送出されることになります。C++ には、
tryブロック内に関数全体を入れる新機能があります。通常の関数の場合、その効果は関数の本体をtryブロック内に入れることと変わりません。しかし、コンストラクタの場合、こうすることでtryブロックはコンストラクタのクラスの基底クラスとメンバーの初期設定子を回避する例外をすべてトラップするようになります。関数全体がtryブロックで囲まれる場合、そのブロックは「関数tryブロック」と呼ばれます。次の例では、基底クラス
BまたはメンバーEのコンストラクタから送出される例外はすべて T コンストラクタの本体に入る前に捕獲され、一致するcatchブロックにより処理されます。
catchブロックは関数の外にあるため、関数tryブロックのハンドラ内でreturn文は使用できません。exit()を呼び出して例外を送出するか、あるいはterminate()を呼び出してプログラムを停止するしかありません。
例外を無効にする
プログラム内で例外を使用しないことがわかっている場合は、コンパイラオプション
-features=no%exceptを使用して、例外処理を行うためのコードが生成されないように設定できます。このオプションを使用すると、コードのサイズが幾分小さくてすむほか、コードの実行が速くなります。しかし、例外を無効にしてコンパイルされたファイルが例外を使用するファイルにリンクされる場合は、例外を無効にしてコンパイルされたファイル内の一部の局所的なオブジェクトは例外発生時に破棄されません。デフォルトでは、コンパイラは例外処理を行うためのコードを生成します。時間と容量のオーバーヘッドが重要でないかぎり、例外を有効にすることをお勧めします。実行時関数と事前定義された例外の使用
標準ヘッダー
<exception>には、C++ 標準に規定されたクラスおよび例外に関連する関数が含まれています。このヘッダーにアクセスできるのは、標準モードで (コンパイラのデフォルトモード、あるいはオプション-compat=5を使用して) コンパイルする場合だけです。次に、<exception>ヘッダーファイル宣言を示します。
標準クラス
exceptionは、選択されている言語構造または C++ 標準ライブラリによって送出されるすべての例外の基底クラスです。exception型のオブジェクトについては、例外を生成することなく生成、コピー、破棄が可能です。仮想メンバー関数what()は、例外を説明する文字列を返します。C++ リリース 4.2 で使用される例外との互換性を保つため、標準モードで使用できるヘッダー
<exception.h>も用意されています。このヘッダーファイルには、標準 C++ コードへの移行のために、標準の C++ の一部ではない宣言も含まれています。開発スケジュールが許す場合は、<exception.h>ではなく<exception>を使用して C++ 標準に準拠するようにコードを更新してください。
互換モード (
-compat[=4]) では、ヘッダー<exception>は使用できません。このモードでは、ヘッダー<exception.h>は C++ リリース 4.2 が提供するものと同じヘッダーを参照します。このヘッダーはここでは掲載していません。シグナルによる例外と setjmp/longjmp の混在
例外どうしの間に関連性がない限り、例外が発生するプログラムでも
setjmp/longjmpを使用できます。例外と
setjmp/longjmpに対する規則は、これらを別々に使用する場合とまったく同様に適用できます。さらに、ポイント A からポイント B へのlongjmpが有効であるのは、ポイント A から送出されてポイント B で捕獲される例外が同じ効果を持つ場合だけです。特に、tryブロックまたはcatchブロックの中へ、あるいは、tryブロックまたはcatchブロックから外へのlongjmpは、直接的にも間接的にも、使用してはなりません。また、自動変数または一時変数の初期化または明示的な破棄の後ろへのlongjmpも使用してはなりません。例外を含む共有ライブラリの作成
共有ライブラリが
dlopen()によって開かれている場合は、RTLD_GLOBALを使用して例外処理を実行する必要があります。
注 - 例外機能を含む共有ライブラリを構築する場合、オプション-Bsymbolicをldに渡さないでください。捕獲されるべき例外が見つからなくなる場合があります。
|
サン・マイクロシステムズ株式会社 Copyright information. All rights reserved. |
ホーム | 目次 | 前ページへ | 次ページへ | 索引 |