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. |
ホーム | 目次 | 前ページへ | 次ページへ | 索引 |