例外処理とは?
例外処理は、エラー処理に似ている機能です。
例えば、C言語でエラー処理するとしたら、
以下のような感じになります。
int result = func(); // func関数でエラーになると0を返すとする。
if (result == 0) {
エラー処理
}
上記のようなif文等によるエラー処理記述は、
ソースファイルの至るところに記述されるため、
エラー処理ではない”プログラム本来の記述”と
区別するのが難しくなってくる場合があります。
こういった場合のためにC++では、
エラー処理の役目を、例外処理と言う機能として用意しています。
上記で示したC言語によるエラー処理を、
C++の例外処理を使って表現すると以下のような感じになります。
int result = 0;
try {
result = func();
if (result == 0) {
throw "func関数でエラーが起きました。";
}
}
catch (char* e) {
cout << e << '\n';
}
例外処理では、try(トライ)ブロックとcatch(キャッチ)ブロックを
1セットとして記述します。
tryブロックでは、tryブロックで囲われたプログラム中で
例外が発生しているかどうか
(エラー等が起こっているかどうか)チェックします。
つまり、例外(エラー等)が無いかtry(試す)と言う意味です。
例外を発生させる場合には、throw(スロー)キーワードを使います。
tryブロック内の処理で例外(エラー等)が発生した時には、
throwを使い、例外が起こった時の情報として、
文字列や数値、オブジェクトなどを
対応するcatchブロックに届ける事ができます。
この例の場合は、文字列 “func関数でエラーが起きました。”
をcatchに届けます。
throwと言うキーワードは、
例外情報をthrow(投げる)と言う意味です。
tryブロックで例外が発生すると、throw文以降の処理が実行されずに
対応するcatchブロックに処理が移ります。
そして、catchブロックでは、
throwされた例外情報が関数の引数のように渡され、
その情報によっていろんなエラー対応処理を記述する事が可能になります。
catchブロックが処理された後は、
一つのtry・catchブロックを抜け、順番通りにプログラムが進行します。
間接的に例外を発生させる
tryブロック内に直接throw文を書かずに、
tryブロック内の関数の中でthrow文を書く、と言った事も可能です。
void func() {
~何かの処理~
if (エラー発生) {
throw "func関数でエラーが起きました。";
}
}
try {
func(); // tryブロック内にthrowがないが、
} // func関数内にthrowがある。
catch (char* e) {
cout << e << '\n';
}
異なる型による複数の例外処理
throw文では、例外情報として、文字列や数値、オブジェクトなどを
対応するcatchブロックに届ける事ができます。
と説明しました。
それぞれの異なる型による例外情報は、
それぞれの型に対応するcatchブロックを記述して対応します。
例えば、例外発生時の情報がint型の数値なら
int型の引数を持つcatchブロックに情報が渡されます。
try {
if (エラー1) throw 1;
if (エラー2) throw "エラー2";
}
catch (int e) {
cout << e << '\n';
}
catch (char* e) {
cout << e << '\n';
}
この場合、整数値による例外情報と、
文字列による例外情報がthrow文で投げられる可能性があるので、
catchブロックは、対応するために、
int型・char*型に対応する2つのcatchブロックを記述しています。
整数値が投げられたら、整数値対応のcatchブロックへ、
文字列が投げられたら、文字列対応のcatchブロックへ、
処理が移ります。
その他の型による例外処理
前述では、整数型と文字列型による例外処理を示しましたが、
どの型にも適さない型が投げられた場合、どうなるでしょうか。
結果は強制終了してしまいます。
こんな時のために、
どんな型情報でも受け取れるcatchブロックの記述方法があります。
try {
if (エラー1) throw 1;
if (エラー2) throw "エラー2";
if (エラー3) throw 1.23;
}
catch (int e) {
cout << e << '\n';
}
catch (char* e) {
cout << e << '\n';
}
catch (...) {
cout << "不明な例外が発生。" << '\n';
}
このプログラムでは、
エラー3の時に小数型の数値が投げられています。
しかし、小数型に対応するcatchブロックは記述されていません。
ですが、catchブロックの引数に…と書く事により、
switch文のdefault句のような役目を果たします。
つまり、例外情報がint型のcatchブロックと
char*型のcatchブロックのどちらにも当てはまらない時は、
…と記述されているcatchブロックに処理が移ります。
この場合、引数(例外発生時の情報)は受け取れません。
この…と記述するタイプのcatchブロックは
複数のcatchブロックの最後に書きます。
catchブロックの引数名の省略
これまでの説明では、
catchブロックに引数のタイプと引数名を記述してきましたが、
引数名は省略する事もできます。
省略する事により、その引数を使う事はできなくなりますが、
catchブロック内でその引数を使わない場合には効率的です。
try {
if (エラー1) throw 1;
if (エラー2) throw "エラー2";
if (エラー3) throw 1.23;
}
catch (int) {
cout << "int型例外が発生" << '\n';
}
catch (char*) {
cout << "char*型例外が発生" << '\n';
}
catch (...) {
cout << "不明な例外が発生" << '\n';
}
例外処理のネスト
例外処理ブロックのtry~catchブロックは
多重構造化する事が可能です。
つまり、tryブロックの中にtry~catchブロックがある状態です。
めんどくさそうですが、
1つ1つ見ていけばそれほど複雑ではありません。
// try1
try {
if (エラー1) throw 1;
// try2
try {
if (エラー2) throw 2;
if (エラー3) throw 3;
}
// catch2
catch (int i2) {
cout << "int型例外が発生" << '\n';
if (i2 == 3) throw;
}
}
// catch1
catch (int i1) {
cout << "int型例外が発生" << '\n';
}
このプログラム例の場合は、
まず、try1内で直接throwされた時は、一番外側のcatch1ブロックへ、
try2内で直接throwされた時は、catch2ブロックへ処理が移ります。
次に、エラー3の例外が発生した場合は、
catch2ブロック内のif文に引っかかり、throwされていますが、
“throw”と言う記述だけです。
この記述は、例外の再スローと言い、
1つ上位のcatch1ブロックへ処理が移ります。
この時には、
再スローされたcatch2ブロック内で取得していた引数値も渡されます。
例外の再スローは、現在の場所(catch2ブロック内)では
エラー対応処理するのに必要な情報が足りない。
と言った場合などに用いられます。
例外指定子
try~catchブロックでは様々なデータ型によるthrowを行えますが、
例外指定子と言う機能を使うと、
指定されたデータ型以外のthrowはできなくなります。
また、例外指定子は関数単位で設定します。
void test() throw (int);
void main() {
test();
}
void test() {
try {
if (エラー1) throw 1;
}
catch (int) {
cout << "int型例外が発生" << '\n';
}
}
このプログラムではtest関数に例外指定子を指定しています。
この場合、throw (int)となっているので、
test関数内でthrowできるデータ型は、int型のみとなります。
それ以外のデータ型によるthrowはできません。
また、複数のデータ型を指定する事も可能です。
void test() throw (int, char*, double);
例外指定子はバグ発生率の低下等に活躍し、
プログラムを、ある程度分かりやすくする効果があります。
また、第3者が作った例外指定子付きの関数を使う場合で、
その関数のエラー処理を行う場合は、
記述されている例外指定子のデータ型だけに対応する例外処理を書けば良い。
と言う意味で受け取れるので、分かりやすいと思います。
例えば、下の例だとtest関数はint型とchar*型を例外として
発生させる可能性があるので、
int型とchar*型を引数としたcatchブロックだけを
記述すればエラー処理できる。と言った具合です。
void test() throw (int, char*);
空のオブジェクトによる例外処理の活用法
例外発生時にスローできるデータ型は、
C++言語の既存データ型(int型等)とユーザー定義型があります。
例外発生時にどういった内容のエラーなのかを
簡潔に知らせる方法として、オブジェクト名を使う方法があります。
特徴としては次の2つです。
・オブジェクト名にエラー内容を推測できる名前を付け、
オブジェクト自体は空にする。
・このオブジェクトは例外処理専用のユーザー定義型として機能させる。
class err1 { }; // 空のオブジェクト
class err2 { }; // 空のオブジェクト
void main() {
try {
if (エラー1) throw err1();
if (エラー2) throw err2();
}
catch (err1) {
cout << "err1例外が発生" << '\n';
}
catch (err2) {
cout << "err2例外が発生" << '\n';
}
このプログラムでは、空のクラスerr1とerr2が定義されています。
エラー1の時は、
エラー1と言うエラー内容が推測できるerr1型をスローしています。
エラー2の時も同様です。
経験豊富なプログラマが良く使う方法のようです。