例外処理

例外とは

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では以下のような画面になります。
(見た目はバージョンにより異なります)
Visual Stuidoでの例外発生画面1

「ハンドルされない例外」というのが発生します。
これは、実行した関数内で不正な処理を検出したので意図的にプログラムを停止させた、という意味です。
正確に言えば、不正な処理への対処が見つからないためプログラムが停止されています。

表示されたダイアログを見ると、どのような例外が発生したかが書かれています。
(赤線の部分。詳しくは後述)
このような画面が出たら「中断」をクリックして実行を停止します。
例外が発生した具体的な箇所のファイルのタブが自動的に開かれますので、自分が作成したソースファイル以外が開かれている場合はタブを閉じてしまいましょう。
Visual Stuidoでの例外発生画面2

このコードのままでは正常にプログラムを実行できません。
これを例外処理するように変更したコードが以下です。


#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

mainfuncAfuncBの階層構造で関数を実行しています。
「funcA」や「funcB」の文字列を表示する行が実行されていないことに注目してください。
関数funcB内ではtryを使用していないので、例外が発生するとその場で関数funcBを抜けます。
funcBの呼び出し元である関数funcAfuncB呼び出し行に制御が移りますが、ここにも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>