プログラム・プロブレム

第四回 例外(C++)


I はじめに

初(以下H):よう、また会ったな、初(はじめ)だぜ。
涼(以下S):涼(すず)ですよ。こんにちは。
H:今回は「例外」について話していくぞ。
S:普通はエラー処理に使われる機能です。ちょっとややこしいかも。

II ソースコード

01:// exception.cpp
02:#include <iostream>
03:int main(int argc,char* argv[])
04:{
05:  try {
06:    std::cout << "passed." << std::endl;
07:    throw( "exception occured." );
08:    std::cout << "not reached." << std::endl;
09:  }catch(const char* e) {
10:    std::cout << "catch exception : " << e << std::endl;
11:  }
12:  std::cout << "finished." << std::endl;
13:
14:  return 0;
15:}
H:ここでのポイントは05、07、09行目だ。
S:新しいキーワードの「try」「throw」「catch」が出てますね。

III 解説

05:  try {
06:    std::cout << "passed." << std::endl;
07:    throw( "exception occured." );
08:    std::cout << "not reached." << std::endl;
H:まずは05行目に「try {」を通るが、ここは新しいブロックを開くだけで何もしない。
S:ただし、ブロック開始の「{」の前に「try」があるのが重要です。
H:そのまま06行目が実行されると、「passed.」が表示される。
S:そして07行目に来ます。ここで「throw( "…" );」が実行されるわけですが…
H:この次で、普通なら08行目に来て「not reached.」が表示されるはずだ。
S:ところが、実行結果には「not reached.」はありません。
H:何故かと言うと、07行目の「throw」によって処理がジャンプするからなんだな。
S:throw が実行されると、引数に渡された値を保存してジャンプが始まります。
H:今回だと "exception occured." が保存されるわけだ。
S:ジャンプ先はコールスタックを逆順に辿って最初の「try」ブロックです。

H:サンプルではエントリポイントである main 関数が一番内側(かつ一番外側)のスタックになるな。
S:throw から逆にスタックを辿ると、最初の try ブロックは05から08行目になります。
H:次に、対象の try ブロックに付随する catch 節を順に見ていく。
09:  }catch(const char* e) {
10:    std::cout << "catch exception : " << e << std::endl;
11:  }
S:この例では09行目にだけ catch 節が記述されていますね。
H:ここで、さっき保存しておいた値(throw の引数)の型と catch 節の型を比較するんだ。
S:一致した catch 節が見付かれば、そのブロックの内容が実行されます。

H:ってワケで08行目をスキップして10行目が実行されることになる。
S:catch 節の動作が終わると、その try-catch ブロックの次から実行が再開されます。
12:  std::cout << "finished." << std::endl;
13:
14:  return 0;
H:例では12行目から再開されるわけだ。これで「finished.」が表示される。
S:最後は「return 0;」で終了というわけです。

最初の try ブロックに、型の合う catch が無かった場合

H:見ての通りだが、こういう場合にどうなるかは気になるよな?
S:型の合う catch 節が無い場合、さらにコールスタックを戻って別の try ブロックを探します。
H:これを型の合う catch に出会えるまで繰り返すわけだ。
S:もしも全部の try を遡っても見付からなかった場合は、一般的にプログラムは強制終了されます。

全部の場合に合う catch

H:何でもいいから throw されたのを全部捕まえたい場合には次のように「catch(...)」を使う。
01:try {
02:  何か
03:}catch(...) {
04:  何か
05:}
S:03行目が全てを捕まえる catch の書き方です。
H:問題は、型に関係無く全部捕まえるから、throw の引数の値を拾えないってことだ。
S:型が何になるか分からないので、扱いようが無いんですね。

例外安全性について

H:さて、ちょっと高度な話になるが、例外でジャンプが起きた時、そのブロックの変数のスコープはどうなるのか?
S:特に try ブロックの中で宣言した変数とかですね。
H:遠くまでコールスタックを遡る場合には、更に多くのブロックを戻ることになる。
S:実は、ブロックを戻る時には、その中の変数のスコープは正常に終了した場合と同様に扱われます。
H:例えば、今回のサンプルの06行目が「int i = 0;」だったして考えてみよう。
S:本来のスコープ終了は09行目の最初なんですけど、07行目の throw でジャンプする時にスコープが終わったことになるわけです。
H:この自動での後始末が重要になる場面があるわけだ。

S:「例外安全である」とは、処理の最中に例外が発生しても問題が無いような書き方がされていることを言います。
H:これだけでは分かりにくいから例を出そう。次のコードを見てくれ。
01:int a_money = 10000;
02:int b_money =     0;
03:try {
04:  何かの処理(例外が起きる可能性がある)
05:  a_money -= 1000;
06:  何かの処理(例外が起きる可能性がある)
07:  b_money += 1000;
08:}catch(...){
09:  std::cout << "exception occured." << std::endl;
10:}
S:このコードでは a さんの口座から b さんの口座に1000円移動したいものとします。
H:上の例だと、05行目で a さんの口座から1000円引いているが、その直後に例外が起こる可能性のある処理を行っている。
S:もし06行目の処理で例外が起きた場合に、b さんへ1000円が加算されていないのに、a さんは1000円引かれたことになっちゃいます。
H:おっと、09行目の前に「a_money += 1000;」を入れてもダメだぜ。04行目で例外が発生した時に a さんの口座が余計に1000円増えるからな。
S:つまり、処理中に例外が発生した時に、問題が発生する恐れがあるわけです。これが例外安全ではないコードです。
H:これが06行目と07行目が逆になってれば、例外が起きても口座残高上の問題は発生しなくなるわけだ。
S:1000円が無事に移動されたか、もしくは全く移動されないかのどちらかになりますからね。
H:かなり作為的なコードだが、例としてはこんなもんだ。実際はクラスの理解が進むと重要になるんだが。
S:とにかく、途中で例外が起きた時でも、できる限り正常な結果を返せるようにするってことなんです。

IV 終わりに

H:これで例外の取り扱いの基本はできたわけだ。
S:例外安全性については「C++ FAQ」という本が詳しいです。参考にしてくださいね。
H:実は一点だけ重要な事をコッソリ端折ったんだが、とりあえずは大丈夫だ。クラスの勉強ができてから話そう。
S:気になる人は「C++ 例外処理 参照渡し」とかで検索してみてください。
H:それじゃ、またな。
S:さようなら。

第四回 終了


一覧に戻る