例外処理
例外とは
C++では、エラー検出の新しい方法として例外という機能が提供されています。
C言語では多くの場合、関数内でのエラーの発生は戻り値にNULLやエラーを示す特定の数値(定数)をセットする、という方法で検出します。
しかし、本来戻り値は関数の処理結果を受け取るものであり、これをエラー発生の有無と混在させるとややこしくなることがあります。
例えば、計算結果を整数で返す関数では、返ってきた値が果たして正しい計算結果なのかエラーなのかを見分けることは困難です。
(正の数しか返さないことが保障される計算ならば、負の数をエラー判定に当てる、などはできます)
また、戻り値は受け取らないことも可能ですから、エラーを戻り値で返してもプログラマが適切に処理するとは限りません。
エラー発生を無視して処理を進めると、より重大なエラーにつながるおそれもあります。
このような問題を解消するのが例外処理です。
例外処理の構文
まずは簡単なサンプルコードを示します。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
//範囲外アクセス
std::cout << vec.at(10) << std::endl;
std::cin.get();
}
要素数が5しかないvectorに対して10番目の要素にアクセスしようとしています。
このコードは当然ながらエラーになるのですが、vectorクラスのat関数は、範囲外の添字が指定された場合に例外を発生させます。
例外が発生すると、Visual Studioでは以下のような画面になります。
「ハンドルされない例外」というのが発生します。
これは、使用した関数内で不正な処理を検出したので、意図的にプログラムを停止させた、という意味です。
表示されたダイアログを見ると、どのような例外が発生したかが書かれています。
(赤線の部分)
このような画面が出たら「中断」をクリックし、現在開いているタブを閉じてしまいましょう。
このコードのままでは正常にプログラムを実行できません。
上のコードを例外処理するように変更したものが以下です。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
try
{
//範囲外アクセス
std::cout << vec.at(10) << std::endl;
std::cout << "正常終了" << std::endl;
}
catch (std::out_of_range &e)
{
std::cout << "範囲外へのアクセスです" << std::endl;
std::cout << e.what() << std::endl;
}
std::cout << "プログラムの終了" << std::endl;
std::cin.get();
}
範囲外へのアクセスです invalid vector<T> subscript プログラムの終了
tryブロック
例外処理は、エラーが発生する可能性のある処理をtryというブロックで囲います。
実際にエラーが発生すると、その時点で処理を中断しcatchというブロックに処理が移ります。
catchブロック内の処理が終わったら、通常通り次の行へと処理が移ります。
上のコードは、プログラムが途中で終了することなく、無事最後の「プログラムの終了」という行まで実行されます。
適切に例外処理をすると、プログラムが途中で終了することなく処理を続けることができます。
catchブロック
catchには引数を指定します。
この引数には先ほどのエラー画面に表示されていたout_of_rangeを指定します。
これは例外オブジェクトといい、発生した例外によって様々な種類が存在します。
(例外オブジェクトはすべてstd名前空間に存在します)
例外オブジェクトは参照で受け取るのが良いとされています。
ポインタや値渡し(コピー)で受け取ることも可能ですが、推奨されません。
その理由はちょっと難しいのでここでは説明しません。
例外オブジェクト
例外オブジェクトは、どのような例外が発生したかの情報が含まれており、catchブロック内でwhat関数を使用することで取得することができます。
(invalid vector<T> subscript → 直訳で「vectorクラスへの無効な添字」)
上の例ではout_of_rangeを使用しましたが、これは名前からも分かる通り範囲外アクセスの時に使用できる例外オブジェクトです。
例外オブジェクトには様々な種類が用意されており、発生が予想される例外によって使い分けます。
tryブロックでは、catchブロックで指定された種類の例外を捕捉し、それ以外の例外は捕捉しません。
上のコードではtryブロック内でout_of_range以外の例外が発生してもcatchブロックに処理は移りません。
C++標準の例外オブジェクトはすべてexceptionクラスを継承したクラスになっていますので、以下のように書くこともできます。
(多様性)
try
{
//範囲外アクセス
std::cout << vec.at(10) << std::endl;
std::cout << "正常終了" << std::endl;
}
catch (std::exception &e)
{
std::cout << "範囲外へのアクセスです" << std::endl;
std::cout << e.what() << std::endl;
}
ただし、このように書くとtryブロック内で発生する(C++標準の)すべての例外を捕捉するようになります。
サンプルでは「範囲外へのアクセスです」という文字列を出力するようにしていますが、out_of_range以外の例外が発生してもcatchブロックは実行され「範囲外へのアクセスです」と出力されます。
例外は、本来は発生したエラーに対して対処する処理を書き、プログラムを停止させないようにするためのものです。
サンプルコードのように、エラーログを出力しただけでは根本的な解決にはなっておらず、そのままプログラムの実行を続けるとより重大なエラーを引き起こす可能性があります。
そのため、大雑把にすべてのエラーを捕捉するのではなく、個々のエラーに対応した処理を書くのが望ましいです。
catchブロックを複数書く
ひとつのtryブロックに対してcatchブロックは複数定義することができます。
try
{
}
catch (std::out_of_range &e)
{
//範囲外アクセス
}
catch (std::exception &e)
{
//C++標準の例外オブジェクトをすべて捕捉
}
catch (...)
{
//その他すべての例外を補足
}
このサンプルでは、まず範囲外アクセス(out_of_range)が発生した時に実行するcatchブロックを定義しています。
次にC++標準の例外(exception)が発生した場合に実行するブロックを定義しています。
if-else if文のように、最初に例外を補足できたcatchブロックのみが実行されます。
「catch (...)」というのはすべての例外を補足するための特別な書き方です。
例外オブジェクトは自作することができますから、「catch (std::exception &e)」だけではC++標準の例外オブジェクト以外は捕捉できません。
(ただしexceptionクラスを継承するクラスを自作した場合は捕捉可能です)
「catch (...)」であれば、いかなる例外であってもこのブロックで捕捉することができるようになります。
ただし、引数がありませんから、例外オブジェクトをcatchブロック内で使用することはできません。
リソースの開放
tryブロック内で例外が発生すると、tryブロック内のその後の行は実行されません。
最初のサンプルコードでは、at関数の後の行(「正常終了」の表示)が実行されていないことに注目してください。
これはつまり、tryブロック内でメモリを確保→破棄処理や、ファイルのオープン→クローズ処理などを書くと、適切な終了処理(リソースの開放)が為されないままcatchブロックに処理が移ってしまう可能性があるということです。
ではcatchブロック内で終了処理を書けば良いのかというと、そういうわけにもいきません。
例外が発生しなければcatchブロックは実行されません。
このような場合には以下のようにします。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
try
{
class IntArr
{
int *arr;
public:
IntArr() : arr(new int[10])
{}
~IntArr()
{
//デストラクタはインスタンスが
//破棄されるときに呼び出される
delete[] arr;
}
};
//このインスタンスの寿命はtryブロック内
IntArr arr;
//参考: 単純にnewした場合
int * arr2 = new int[10];
//範囲外アクセス
std::cout << vec.at(10) << std::endl;
//これより以下の行は実行されない
std::cout << "正常終了" << std::endl;
//つまりこのdeleteは実行されない
delete[] arr2;
}
catch (std::out_of_range &e)
{
std::cout << "範囲外へのアクセスです" << std::endl;
std::cout << e.what() << std::endl;
}
std::cin.get();
}
例えばメモリを確保するような処理を書く場合は、それ専用のクラスを用意します。
そのクラスのインスタンスをtryブロック内で生成すると、インスタンスの寿命はtryブロックを抜けるまでとなります。
(newでインスタンスを生成しない場合)
例外が発生するしないに関わらずtryブロックを抜ける時に必ずデストラクタが呼ばれますので、解放処理が実行されない可能性を排除することができます。
このサンプルコードでは、クラスの定義をtryブロック内に記述しています。
クラスや構造体などの定義はグローバル(関数外)で行わなければならないという決まりはないため、このコードのように特定のブロック内に記述することもできます。
特定のブロック内で定義すると、そのクラスを使用できるのはそのブロック内のみとなります。
特定の場所でしか使用されないクラスはスコープを限定し他からアクセスできないようにすると保守性が高まります。
ただ、この方法はわざわざクラスを用意せねばならず、やや面倒くさいです。
サンプルコードのような処理であればスマートポインタを使用したほうが良いでしょう。
例外を自作する
例外はあらかじめ用意された物以外にも、自分で定義することができます。
#include <iostream>
double average(const int *arr, int len)
{
if (len <= 0)
throw "引数lenが0以下";
int tmp = 0;
for (int i = 0; i < len; i++)
tmp += arr[i];
return tmp / static_cast<double>(len);
}
int main()
{
int arr[] = { 1, 2, 3, 4 ,5 };
try
{
std::cout << average(arr, -1) << std::endl;
}
catch (const char *s)
{
std::cout << s << std::endl;
}
std::cin.get();
}
引数lenが0以下
throwというキーワードを使用すると、その場で例外を意図的に発生させることができます。
(例外を投げると言います)
引数チェックくらいで例外を発生させることはあまりありませんが、例えば予期しない計算結果になった場合などに例外を発生させるなどが考えられます。
throw文に続いて例外オブジェクトを指定します。
これはC++標準の例外オブジェクトでも構いませんし、文字列や数値、自作クラスなどでも構いません。
ここで指定した例外オブジェクトと、catch文の引数のデータ型とが一致した時に、例外が捕捉されcatchブロックが実行されるようになります。
例外発生時の処理の移動
例外が発生するとその場で処理を停止し、throw文を取り囲むtryブロックに続くcatchブロックに処理が移ります。
ただし、例外オブジェクトがcatchブロックの引数で指定した例外オブジェクトの種類と一致しなければ捕捉されません。
関数から別の関数を呼び出すなど、呼び出しが深い場所にあってもtryブロックが見つかるまで関数を抜け続けます。
#include <iostream>
void funcB(bool b)
{
if (b)
throw "例外:funcB";
std::cout << "funcB" << std::endl;
}
void funcA(bool b)
{
funcB(b);
std::cout << "funcA" << std::endl;
}
int main()
{
try
{
funcA(true);
}
catch (const char *s)
{
std::cout << s << std::endl;
}
std::cin.get();
}
例外:funcB
「funcA」や「funcB」を表示する行が実行されていないことに注目してください。
関数funcB内で例外が発生すると、その時点で関数funcBを抜けます。
funcBの呼び出し元である関数funcAに制御が移りますが、ここにはtryブロックが存在しないので、直ちに関数funcAも抜けます。
main関数まで戻ってきてようやくtryブロックが見つかるので、続くcatchブロックが実行されます。
tryブロックが見つかるまで連続して関数を抜けることができるので、C言語のように戻り値でエラーを返す方法よりも簡単かつ確実にエラー処理を行うことができます。
tryブロックが見つからない場合
main関数まで戻ってきてもtryブロックを見つけることができなかった場合、std::terminate関数が実行されます。
この関数はプログラムを異常終了させるabord関数を呼び出すだけの関数です。
例外の再送出
catchブロック内でthrowを書くと、そのままさらに例外を発生させます。
#include <iostream>
void funcB(bool b)
{
if (b)
throw "例外:funcB";
}
void funcA(bool b)
{
try
{
funcB(b);
}
catch (const char*)
{
std::cout << "例外の再送出:funcA" << std::endl;
throw;
}
}
int main()
{
try
{
funcA(true);
}
catch (const char *s)
{
std::cout << s << std::endl;
}
std::cin.get();
}
例外の再送出:funcA 例外:funcB
throw文には例外オブジェクトを何も指定しません。
こうすることで、受け取った例外オブジェクトをそのまま上位のtryブロックに渡すことができます。