例外処理
例外とは
C++では、実行時エラー検出の新しい方法として例外という機能が提供されています。
C言語でのプログラム実行時のエラーは、例えば関数の戻り値でエラーを示す値を返して、呼び出し側で値をチェックするという方法があります。
その他、errno
という特殊なグローバル変数にエラーを示す値をセットし、関数の実行後にerrno
の値をチェックするという方法もあります。
これらの方法の最大の欠点は、関数がエラーを返してもプログラマが適切にこれらの値をチェックしエラーに対処しない可能性があることです。
エラーが発生しているのにそのまま処理を続行すると、最悪の場合はデータ破損などの重大な事態に繋がる可能性があります。
また、エラー値を戻り値として返す関数は、戻り値がエラーを示す場合と正常な処理の結果としての値である場合とが混在することになり、見分けが付きにくくなります。
このような問題を解消するのが例外処理です。
例外処理の構文
まずは実行時エラーを起こす簡単なサンプルコードを示します。
#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番目の要素にアクセスしようとしています。
管理する領域外へのアクセスはコンパイル時には検出できず、実行時エラーとなります。
ちなみにat関数でアクセスしていますが、添字演算子([]
)でアクセスした場合はプログラムはエラーを検出しません。
もちろん管理しない領域へのアクセスは危険で、それによりプログラムが停止する可能性もあります。
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
というブロックに処理が移ります。
例外が発生せずにtry
ブロックの終端まで進んだ場合はcatch
ブロックは無視されます。
上のコードは、プログラムが途中で停止することなく最後の「プログラムの終了」という行まで実行されます。
適切に例外処理をすると、プログラムが途中で停止することなく処理を続けることができます。
catchブロック
catch
には引数を指定します。
今回は先ほどのエラー画面に表示されていたstd::out_of_range
を指定します。
これは例外クラスといい、std::out_of_range
は範囲外アクセスが発生したときに使用される例外クラスです。
これ以外にも例外の種類に応じた様々な例外クラスが存在します。
例外クラスのインスタンスを例外オブジェクトと言い、これをcatch
ブロック内で使用します。
なお、C++標準の例外クラスはすべてstd
名前空間に存在します。
例外オブジェクトは参照で受け取ります。
ポインタや値渡し(コピー)で受け取ることも可能ですが、推奨されません。
例外オブジェクト
例外オブジェクトにはどのような例外が発生したかの情報が含まれており、catch
ブロック内でwhat
メンバ関数を使用することで取得することができます。
今回表示された「invalid vector<T> subscript」は、直訳すると「vectorクラスへの無効な添字」という意味です。
(出力される内容はコンパイラにより異なります)
上の例ではout_of_range
を使用しましたが、これは名前の通り範囲外アクセスの時に使用できる例外クラスです。
例外クラスには様々な種類が用意されており、発生が予想される例外によって使い分けます。
try
ブロックでは、catch
ブロックの引数で指定された種類の例外を捕捉し、それ以外の例外は捕捉しません。
上のコードではtry
ブロック内でout_of_range
以外の例外が発生してもcatch
ブロックに処理は移りません。
C++標準の例外クラスはすべてstd::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 (...)
{
//その他すべての例外を捕捉
}
このサンプルでは、まず範囲外アクセス(std::out_of_range
)が発生した時に実行するcatchブロックを定義しています。
次にC++標準の例外(std::exception
クラス)が発生した場合に実行するブロックを定義しています。
これはif-else文のように、最初に例外を捕捉できたcatch
ブロックのみが実行されます。
最後のcatch (...)
というのはすべての例外を捕捉するための特別な書き方です。
例外オブジェクトは自作することができるので、catch (std::exception &e)
だけではC++標準の例外オブジェクト以外は捕捉できません。
(exceptionクラスを継承するクラスを自作した場合は捕捉可能です)
catch (...)
であれば、いかなる例外であってもこのブロックで捕捉することができます。
ただし引数がありませんから、例外オブジェクトをcatch
ブロック内で使用することはできません。
リソースの解放
try
ブロック内で例外が発生すると、try
ブロック内のその後の行は実行されません。
最初のサンプルコードでは、at
関数の後の行(「正常終了」という文字の出力)が実行されていないことに注目してください。
これはつまり、try
ブロック内でメモリを確保→破棄処理や、ファイルのオープン→クローズ処理などを書くと、適切な終了処理(リソースの解放)が行われずにcatch
ブロックに処理が移ってしまう可能性があるということです。
ではcatch
ブロック内で終了処理を書けば良いのかというと、そういうわけにもいきません。
例外が発生しなければcatch
ブロックは実行されません。
対処方法はいくつかあります。
全てのcatchブロックに解放処理を書く
try
ブロックの最後と、catch (...)
ブロックも含めた全てのcatch
ブロックに解放処理を書くのが最も単純な方法です。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
int* arr;
try
{
//メモリの動的確保
arr = new int[10];
//範囲外アクセス
std::cout << vec.at(10) << std::endl;
//これより以下の行は実行されない
std::cout << "正常終了" << std::endl;
//つまりこのdeleteは実行されない
delete[] arr;
}
catch (std::out_of_range& e)
{
//全てのcatchブロックに解放処理を書く
delete[] arr;
std::cout << "範囲外へのアクセスです" << std::endl;
std::cout << e.what() << std::endl;
}
catch (...)
{
//catch (...)ブロックにも解放処理が必要
delete[] arr;
}
std::cin.get();
}
考え方としては分かりやすいですが、同じ処理を何度も書かなければならないのが欠点です。
リソースを管理する専用のクラスを作る
リソースを確保/解放する処理を行う専用のクラスを作る方法もあります。
#include <iostream>
#include <vector>
//int型を動的に確保するクラス
class IntArr
{
int* arr;
public:
IntArr(size_t n) : arr(new int[n])
{}
~IntArr()
{
//デストラクタはインスタンスが
//破棄されるときに呼び出される
delete[] arr;
}
};
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
try
{
//このインスタンスの寿命はtryブロック内
IntArr arr(10);
//参考: 単純に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;
}
//例外の発生に関係なく、この時点でインスタンスarrは破棄されている
//IntArrのデストラクタにより確保しているメモリも解放されている
std::cin.get();
}
専用クラスのインスタンスをtry
ブロック内で生成すると、インスタンスの寿命はtry
ブロックを抜けるまでとなります。
(new
でインスタンスを生成しない場合)
例外が発生するしないに関わらずtry
ブロックを抜ける時に必ずデストラクタが呼ばれるので、解放処理が実行されない可能性を排除することができます。
この方法の欠点はわざわざクラスを用意しなければならない点です。
面倒ですし、コードが長くなりがちです。
長所としては、自分で解放処理を自由に書けるのであらゆるリソースに対して対応できます。
スマートポインタを利用する
スマートポインタを利用するとコードが簡潔になります。
#include <iostream>
#include <vector>
#include <memory>
int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };
try
{
auto arr = std::make_unique<int[]>(10);
//↑と↓は同じ意味
//std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
//範囲外アクセス
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;
}
//例外の発生に関係なく、この時点でunique_ptrのインスタンスは破棄されている
//unique_ptrが管理しているメモリも解放されている
std::cin.get();
}
解放処理が複雑になる場合はdeleterを定義することになるため、手間としては自作クラスを作る場合とあまり変わらなくなります。
しかし多くの場合ではこれが最もバランスの良い方法かもしれません。
例外を意図的に発生させる
throw
式を使用すると、例外を意図的に発生させることができます。
#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;
std::cout << "例外発生なし" << std::endl;
}
catch (const char* s)
{
std::cout << "例外発生" << std::endl;
std::cout << s << std::endl;
}
std::cin.get();
}
例外発生 引数lenが0以下
throw
はその場で例外を発生させます。
(例外を投げる(スローする)と言います)
throw
キーワードに続いて例外オブジェクトを指定します。
これはC++標準の例外オブジェクトでも構いませんし、文字列や数値、自作クラスなどでも構いません。
ここで指定した例外オブジェクトのデータ型とcatch
文の引数のデータ型とが一致した時に、例外が捕捉されcatch
ブロックが実行されるようになります。
例外発生時の処理の移動について
例外が発生するとその場で処理を停止し、例外発生個所を取り囲むtry
ブロックに続くcatch
ブロックに処理が移ります。
(ただし、例外オブジェクトがcatch
ブロックの引数で指定した例外オブジェクトの種類と一致しなければ捕捉されません)
例外発生個所がtry
ブロック内でない場合、その場で関数の処理を停止し関数を抜けます。
関数から別の関数を呼び出していた場合など、深い階層で例外が発生した場合でも、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
main
→funcA
→funcB
の階層構造で関数を実行しています。
「funcA」や「funcB」の文字列を表示する行が実行されていないことに注目してください。
関数funcB
内ではtry
を使用していないので、例外が発生するとその場で関数funcB
を抜けます。
funcB
の呼び出し元である関数funcA
のfuncB
呼び出し行に制御が移りますが、ここにも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
ブロックで発生した例外オブジェクトをそのまま関数呼び出し元のtry
ブロックに渡すことができます。
これを例外の再送出(リスロー)といいます。
例外オブジェクトを指定しないthrow
式をcatch
ブロック以外の場所に書くと、捕捉不可能な例外となりプログラムが異常終了します。
C++標準の例外クラス
C++標準の例外クラスは全てstd::exception
クラスを基底クラスとした派生クラスです。
ただし直接の派生クラスではなく、派生クラスの派生クラス(孫クラス)であったりする場合もあります。
全てstd
名前空間に属しますが、定義されているヘッダファイルは異なります。
std::exception
クラス自体は<exception>
ヘッダファイルに定義されています。
例外クラス | 説明 | ヘッダファイル | |
---|---|---|---|
logic_error | 論理エラー プログラムの実行前に検出可能な類のエラーの基底クラス。 |
<stdexcept> | |
├ | domain_error | 定義域エラー 定義される範囲外の値が渡された。 ドメインとは「領域」の意味。 他のlogic_error派生の例外クラスに該当しない場合に使用する。 C++標準ライブラリではこの例外は送出しない。 |
<stdexcept> |
├ | invalid_argument | 不正な引数 引数として想定しない値が指定された。 |
<stdexcept> |
├ | length_error | 長さエラー 定義する長さを超える値が指定された。 例えばstringクラスやvectorクラスなどがサポートする最大長を超える場合にこのエラーが送出される。 |
<stdexcept> |
├ | out_of_range | 範囲外の値 定義する範囲外にアクセスしようとした。 例えばvectorクラスが管理する領域外へのアクセス時にこのエラーが送出される。 |
<stdexcept> |
└ | future_error | 同期エラー (C++11以降) std::futureやstd::promiseが動作に失敗した。 |
<future> |
runtime_error | 実行時エラー プログラムの実行時にのみ検出可能な類のエラーの基底クラス。 |
<stdexcept> | |
├ | range_error | 範囲エラー 計算結果が想定する範囲外の値となった。 |
<stdexcept> |
├ | overflow_error | オーバーフローエラー 計算結果がオーバーフローした。 |
<stdexcept> |
├ | underflow_error | アンダーフローエラー 計算結果がアンダーフローした。 |
<stdexcept> |
├ | system_error | システムエラー (C++11以降) OSの機能を使用する関数の失敗。 例えばstd::thread(スレッド機能)などがこの例外を送出する。 |
<system_error> |
└ | ios_base::failure | 入出力エラー 入出力ストリームの操作に失敗した。 C++03まではexceptionクラスを基底クラスとしていたが、C++11からはsystem_errorクラスを基底クラスとするように変更されている。 |
<ios> |
bad_alloc | アロケートの失敗 記憶域の動的確保(new演算子など)に失敗した。 |
<new> | |
└ | bad_array_new_length | 入出力エラー (C++11以降) 動的配列の確保時に、 ・要素数に0未満を指定した。 ・要素数に処理系定義の最大値を超える値を指定した。 ・初期化子の数が要素数を超えている。 |
<new> |
bad_cast | キャストの失敗 dynamic_castが失敗した。 例えば継承関係にないクラスをキャストしようとした場合などに送出される。 |
<typeinfo> | |
bad_exception | 例外の失敗 ・動的例外仕様に違反する例外を送出した。 ・std::current_exception関数の実行時に例外オブジェクトのコピーに失敗した。 |
<exception> | |
bad_typeid | typeidの失敗 typeid演算子にnullptrを適用した。 |
<typeinfo> | |
bad_function_call | std::functionの失敗 (C++11以降) std::functionに関数を設定せずに呼び出した。 |
<functional> | |
bad_weak_ptr | weak_ptrの失敗 (C++11以降) 破棄済みのshared_ptrをコンストラクタに指定してweak_ptrを生成しようとした。 |
<memory> |