例外処理2

コンストラクタの例外処理

クラスのコンストラクタ内で発生した例外をその場で捕捉して処理することは特別なことではなく、他の関数と同じようにできます。

コンストラクタから例外を送出する場合は注意が必要です。
コンストラクタから例外を送出するということは、処理が最後まで終了しておらずインスタンスの生成には失敗しています。


#include <iostream>
#include <string>
#include <stdexcept>

class X
{
    int a;

public:
    X(int a)
    {
        //引数が0未満なら例外発生
        if (a < 0)
            throw std::runtime_error("Argument value is less than 0");
        this->a = a;

        std::cout << a << ": X constructor completed" << std::endl;
    }

    ~X() { std::cout << a << ": X destructor completed" << std::endl; }
};

class C
{
    X* a;
    X* b;

public:
    C() :
        a(new X(1)),
        b(new X(-1)) //例外が発生する引数の指定
    {
        std::cout << ": C constructor completed" << std::endl;
    }

    ~C()
    {
        delete a;
        delete b;
        std::cout << ": C destructor completed" << std::endl;
    }
};

int main()
{
    try {
        C c;
    }
    catch (std::exception& e)
    {
        std::cout << "exception occurred: " << e.what() << std::endl;
    }

    std::cin.get();
}
1: X constructor completed
exception occurred: Argument value is less than 0

main関数内のクラスCのインスタンス生成は失敗するので、catch文が実行されます。
プログラムは停止しないので問題がないように見えますが、インスタンスが生成されていないということはデストラクタも実行されないということです。

コンストラクタではメンバ変数bの初期化時に例外が発生しており、bに値は割り当てられず空のままです。
しかしその手前のメンバ変数aの初期化には成功しているので、aはクラスXのインスタンスを持っています。
最終的にクラスCのコンストラクタは失敗するので、デストラクタは実行されずメンバ変数aに対するdeleteも実行されません。
他からdeleteする手段もないため、解放できないメモリが残ることになります。

解決方法としては、コンストラクタ内で例外処理を行うか、スマートポインタなどのデストラクタが不要な方法に置き換えることです。


class X { /*省略*/ };

//対処法1:コンストラクタでtry-catch
class C1
{
    X* a;
    X* b;

public:
    //メンバ変数は初期化されない場合は不定値
    //不定値に対してdeleteしないように
    //あらかじめnullptrで初期化しておく
    C1() : a(nullptr), b(nullptr)
    {
        try {
            a = new X(1);
            b = new X(-1);
        }
        catch (...)
        {
            delete a;
            delete b;

            //二重解放の防止
            a = nullptr;
            b = nullptr;
        }
        std::cout << "C1 constructor completed" << std::endl;
    }

    ~C1()
    {
        delete a;
        delete b;
        std::cout << "C1 destructor completed" << std::endl;
    }
};

//対処法2:スマートポインタ
class C2
{
    std::unique_ptr a;
    std::unique_ptr b;

public:
    C2()
    {
        a = std::make_unique(1);
        b = std::make_unique(-1);
        //コンストラクタは失敗するのでこれは出力されない
        std::cout << "C2 constructor completed" << std::endl;
    }

    //スマートポインタに解放処理は必要ないので
    //デストラクタでの解放は不要
    ~C2()
    {
        //コンストラクタは失敗するのでこれも出力されない
        std::cout << "C2 destructor completed" << std::endl;
    };
};

int main()
{
    try {
        C1 c1;
    }
    catch (std::exception& e)
    {
        std::cout << "exception occurred: " << e.what() << std::endl;
    }
    
    std::cout << std::endl;

    try {
        C2 c2;
    }
    catch (std::exception& e)
    {
        std::cout << "exception occurred: " << e.what() << std::endl;
    }

    std::cin.get();
}
1: X constructor completed
1: X destructor completed
C1 constructor completed
C1 destructor completed

1: X constructor completed
1: X destructor completed
exception occurred: Argument value is less than 0

コンストラクタ内で例外処理をする場合(クラスC1)はコンストラクタが成功するので、インスタンスが生成されます。
スマートポインタを使用した方(クラスC2)はコンストラクタが失敗する(例外が送出される)ので、呼び出し元で例外処理が必要です。

関数tryブロック

コンストラクタ内にもtryブロックを書くことができますが、その前の処理であるメンバイニシャライザ(委譲コンストラクタ、基底クラスのコンストラクタ)で発生する例外の捕捉は少し特殊な書き方となります。


class X{ /*省略*/ };

class C
{
    X* a;
    X* b;
public:
    //a,bの初期化で発生する例外が捕捉される
    C(X* x, X* y) try : a(x), b(y)
    {
        //ここで発生する例外も捕捉される
    }
    catch (std::exception& e)
    {
    }
    catch (...)
    {
    }
};

メンバイニシャライザを示す:(コロン)の手前にtryを記述します。
tryのブロックはコンストラクタのブロックと共通となります。
catchブロックはその後ろに記述します。
これを関数tryブロックといいます。

このとき、メンバイニシャライザやコンストラクタで例外が発生するとインスンタンスの生成に失敗し、デストラクタも実行されません。
発生した例外はそのまま送出されますので、呼び出し元で捕捉する必要があります。

デストラクタの例外処理

デストラクタからは例外を送出すべきではありません。
例外を送出するということは処理を途中で停止することなので、リソースの解放処理であるデストラクタを途中で停止すると解放されないリソースが残ってしまいます。

それ以上に問題なのが、例外処理中にさらに例外が発生すると必ずプログラムが異常終了することです。
例外が発生すると、catchで捕捉されるまでの間に破棄すべきオブジェクトがあれば破棄されます。
(tryブロック内で宣言したオブジェクトや、関数を抜ける場合は引数とローカル変数が破棄される)
オブジェクトの破棄なのでそのオブジェクトにデストラクタがあれば呼ばれますが、このデストラクタ内でさらに例外が送出されると例外の処理前にさらに例外が発生します。
二重の例外を適切に処理することはできないため、std::terminate関数が呼び出されプログラムは異常終了します。


#include <iostream>
#include <exception>

class C
{
public:
	//「noexcept(false)」はデストラクタから
	//例外を送出するために必要な例外指定
    ~C() noexcept(false)
    {
        //デストラクタから例外を送出
        throw std::runtime_error("C destructor exception");
    }
};

void f()
{
    C c;

    //ここで例外発生
    throw std::runtime_error("except was thrown from f");
    
    //呼び出し元(main関数)で例外が捕捉される前に
    //ローカル変数は破棄される
    //つまり例外の捕捉前にcのデストラクタが実行され、
    //そこでさらに例外が送出されてしまう
}

int main()
{
    try
    {
        f();
		//このコードはこれ以降の行に到達できず異常終了する
    }
    catch (...)
    {
		//この行も出力されない
        std::cout << "any exception occurred" << std::endl;
    }
}

デストラクタからの例外送出は適切に処理することは困難なため、デフォルトでデストラクタは例外を送出してはならない設定になっています。
そのためデストラクタからの例外の例外があると、その時点でプログラムは異常終了します。
(tryでも捕捉できません)
上記コードでは、クラスCのデストラクタにnoexcept(false)という指定がありますが、これはデストラクタからの例外送出を許可するように設定を変更するためのものです。
noexceptについては詳しくは後述します。

デストラクタ内で例外が発生する可能性がある場合、外部には例外を送出せず、デストラクタ内部で例外処理を完結させる必要があります。

なお、例外を捕捉してcatchブロックに処理が移った後に、そのcatchブロック内で例外が発生する事は問題ありません。
この場合はtry-catchブロックを入れ子にすることで対応できます。


void f()
{
    throw std::runtime_error("except was thrown from f");
}

int main()
{
    try
    {
        f();
    }
    catch (...)
    {
        try
		{
            f();
        }
        catch (...)
        {
            std::cout << "nested exception" << std::endl;
        }
        std::cout << "exception" << std::endl;
    }
}
nested exception
exception

noexcept

例外に関係するキーワードにnoexceptがあります。
(C++11以降)
noexceptには二種類の機能があります。

関数の例外仕様の指定子

関数の引数リストの後ろにnoexceptを記述することで、その関数が例外を送出する(外部に例外を投げる)可能性があるか否かを指定することができます。


void f1() noexcept {}			//例外を送出しない
void f2() noexcept(true) {}		//例外を送出しない
void f3() noexcept(false) {}	//例外を送出する可能性がある
void f4() {}					//例外を送出する可能性がある

このnoexceptには引数を指定でき、式が真になる場合は例外を送出しない指定となります。
偽の場合は例外を送出する可能性がある関数の指定となります。
引数を省略した場合は真を指定した場合と同じです。
noexceptがない関数は偽を指定した場合と同じです。
ただしデストラクタはnoexcept(true)がデフォルトで指定されています。
(明示的にnoexcept(false)などを指定することで変更可能です)

例外を送出しない指定をした関数が例外を送出すると、その場でプログラムは異常終了します。
(tryを使っても捕捉できない)
これは安全な終了ではないので、ファイル書き込み中などの場合はデータが破損する可能性があります。
なお、その関数内でtry-catchで例外処理をすることは可能です。

例外を送出しない関数は主に二つの効果があります。

パフォーマンスの向上

関数の呼び出し時には関数からの例外の送出に対応するための内部的な処理が行われるのですが、これを省くことができます。
そのためパフォーマンスの向上が期待できます。

例外安全性の保証

例外を送出しないということはその関数の実行は絶対に成功するということを意味します。
(ここで言う「成功」とは処理が不完全なままで終了しないという意味で、例えば関数は成功した上でnullptrなどの無効な値を返すことはあり得ます)
ある関数で例外が発生する可能性がある場合、関数呼び出し側はそのことを考慮したコードにする必要があります。
例外が発生する可能性がないならばその処理を省くことができ、簡潔かつ高速な処理が可能になります。

ただしこれは関数を使う側からの話で、例外を一切発生させない関数を作るのは結構難しいものです。
単純な四則計算程度ならともかく、C++や関数内で使用する関数の仕様を完全に把握しないと思わぬところで例外が発生するかもしれません。
関数の処理全体をtryブロックで囲ってすべての例外を捕捉すれば外部に例外は送出されませんが、それをするくらいならnoexceptを指定せずに呼び出し側で例外を捕捉するようにしたほうが良いでしょう。
また、プログラムのバージョンアップに伴う仕様の変更でnoexceptを外すと、noexceptを前提としていた他の部分でもコードの修正が必要になる可能性があるので、特に他人に利用してもらうコードを書く場合は注意が必要です。

noexcept演算子

noexceptキーワードにはもうひとつ、演算子としての機能があります。
これは被演算子に指定した式が例外を送出する可能性があるか否かをbool値で返します。
これは主に上述の関数のnoexcept指定子と共に使用されます。


void f1() noexcept {}
void f2() noexcept(false) {}

//関数f1が例外を送出するなら関数g1も例外を送出する
void g1() noexcept(noexcept(f1()))
{
    f1();
}
void g2() noexcept(noexcept(f2()))
{
    f2();
}

int main()
{
	constexpr bool b1 = noexcept(g1()); //true
	constexpr bool b2 = noexcept(g2()); //false

	std::cin.get();
}

ある関数関数の例外送出の可能性が、その関数から呼び出される関数の例外送出可能性に依存する場合はこのように記述することで対応することができます。
noexcept演算子はコンパイル時に実行されるため、constexpr定数にすることができます。