2014/01/17

C++Tips2 例外でエラーハンドリング

こんばんはKです。今日はC++の例外について触れたいと思います。

例外は、その状況下で処理を継続できない、あるいは継続しても意味がない状態のことをいいます。C++では例外を処理するための機構が用意されています。これはtry-catch文と呼ばれています。

throw 値;

とすれば値を実引数として例外を投げられます。そして、catch文で例外を捕捉します。

try {
    // このなかでthrowされる
}
catch(仮引数) {
    // ここで例外をキャッチします
}

引数には様々な方を与えることができますが、一般的に例外オブジェクトを作成することが推奨されています。例外オブジェクトは、標準ヘッダファイルで用意されています。std::exceptionは例外オブジェクトの基本となるものです。これを継承して標準で様々な例外オブジェクトが用意されています。

例えば、new演算子でnewが失敗したときに発生する例外はstd::bad_allocです。これを捕捉することで、new演算子でのメモリ確保の失敗の処理を書けます。

以下の例では例外として0を送出し、catchでint型を受け取っています。本来の例外処理としてはだめな例です。例外オブジェクトを送出するのが基本です。

#include <stdio.h>

int f(int x) {
    if(x == 0) {
        throw 0;
    }
    return 0;
}

int main(void) {
    try {
        f(0);
    }
    catch(int x) {
        printf("0が入力されました。");
    }

    getchar();
    return 0;
}

基本的には例外は、標準にあるものを使うべきです。今回の場合は特定の値が入力されたことに対する例外ですから、std::range_errorが適切でしょう。標準のエラーはstdexceptヘッダファイルに定義されています。よって以下のように書き直すべきです。

#include <stdio.h>
#include <stdexcept>

int f(int x) {
    if(x == 0) {
        throw std::range_error("0が入力されました。");
    }
    return 0;
}

int main(void) {
    try {
        f(0);
    }
    catch(const std::range_error& e) {
        printf(e.what());
    }
    getchar();
    return 0;
}

ここで、const std::range_error&としたのは、無駄なコピーをなくすために参照にし、その値は書き換える必要がないのでconstにしました。(そもそもthrow時に右辺値として生成するオブジェクトなのですから、const参照で受け取ります)

それでも独自の例外を投げたい場合があります。それは、作成したクラス特有の例外である場合がほとんどです。そのクラスを使う人は、そのクラス固有の例外クラスを知っていて、それをcatchすることで適切に動作するプログラムが書けます。

例えば次のような独自クラスMyClassがあったとしましょう。

class MyClass {
public:
    MyClass() {
       
    }
};

このクラスではコンストラクタで「エラー1」を吐くとします。そういうときは、MyClassExceptionというMyClass専用の例外クラスを作ります。更にMyClassExceptionクラスを継承したError1というクラスを作ります。MyClassでは、例外が発生したときにError1を吐くわけです。

MyClassExceptionは、std::exceptionを継承して次のようなクラスになります。

class MyClassException : public std::exception {
public:
    class Error1; // 後で定義する

    MyClassException(const char* message) : message(message) { } // 独自のコンストラクタ

    virtual const char* what() const { // whatメンバ関数はエラー情報を文字列で返すもの
        return message;
    };
private:
    const char* message;
};

Error1は、MyClassExceptionを継承して次のように定義します。

class MyClassException::Error1 : public MyClassException {
public:
    Error1() : MyClassException("エラー1") { } // この例では単なる固定メッセージのみ持たせました
};

実際に例外を発生させるには、throwします。結局、全体のコードは次のようになります。

#include <stdio.h>
#include <stdexcept>

/* MyClassの独自例外クラス */
class MyClassException : public std::exception {
public:
    class Error1;

    MyClassException(const char* message) : message(message) { }

    virtual const char* what() const {
        return message;
    };
private:
    const char* message;
};

/* 独自例外の「エラー1」クラス */
class MyClassException::Error1 : public MyClassException {
public:
    Error1() : MyClassException("エラー1") { }
};

/* 独自クラス */
class MyClass {
public:
    MyClass() {
        throw MyClassException::Error1();
    }
};

/* 独自クラスを使う処理(main) */
int main(void) {
    try {
        MyClass mc; // ここで例外が発生する可能性がある
    }
    // MyClassの例外「エラー1」を捕捉する
    catch(const MyClassException::Error1& e) {
        printf(e.what()); // エラー内容を文字列で取得する
    }

    getchar();
    return 0;
}

例外の種類が増えれば、MyClassExceptionを継承してたくさんの例外を作る必要があります。たくさんの例外が定義されているからと言って、全ての例外を事細かに捕捉する必要はありません。例外の内、特定する必要はないが、失敗した場合とりあえず例外を捕捉したいときがあります。以下の例ではError2が発生したが、Error1以外は特定することに興味がない例です。

#include <stdio.h>
#include <stdexcept>

class MyClassException : public std::exception {
public:
    class Error1;
    class Error2;

    MyClassException(const char* message) : message(message) { }

    virtual const char* what() const {
        return message;
    };
private:
    const char* message;
};

class MyClassException::Error1 : public MyClassException {
public:
    Error1() : MyClassException("エラー1") { }
};

class MyClassException::Error2 : public MyClassException {
public:
    Error2() : MyClassException("エラー2") { }
};

class MyClass {
public:
    MyClass() {
        throw MyClassException::Error2(); // Error2
    }
};

int main(void) {
    try {
        MyClass mc;
    }
    catch(const MyClassException::Error1& e) {
        printf(e.what());
    }
    // 残り全ての「MyClassの例外」が捕捉される
    catch(const MyClassException& e) {
        printf(e.what());
    }


    getchar();
    return 0;
}

このコードのポイントは、MyClassの中で起こった例外は必ず捕捉することにあります。

ここまで例外の基本的な扱い方について言及しました。例外は基本的に、全てのエラー処理が必要なクラスに設けるべきです。そして、例外が発生する可能性のある処理には全てtry-catchで例外をcatchします。ただし、想定する例外のみcatchします。C++でも「catch(...)」とかくことで全ての例外を捕捉できますが、それはやってはいけないことです。分かっている例外に対して、適切な処理を施すのが例外処理です。ほとんどの場合クラス毎に例外を設け、下位の例外をしっかりフォローすべきです。

#include <stdio.h>
#include <stdexcept>

class MyClassException : public std::exception {
public:
    MyClassException(const char* message) : message(message) { }

    virtual const char* what() const {
        return message;
    };
private:
    const char* message;
};

class MyClass {
public:
    MyClass() {
        throw MyClassException("失敗1");
    }
};

class MyClass2Exception : public std::exception {
public:
    MyClass2Exception(const char* message) : message(message) { }

    virtual const char* what() const {
        return message;
    };
private:
    const char* message;
};

class MyClass2 {
public:
    MyClass2() {
        try {
            MyClass mc; // 下位のクラス
        }
        catch(const MyClassException& e) { // 下位の失敗をフォローして
            throw MyClass2Exception(e.what()); // 自分の失敗として上位に報告する
        }

    }
};

int main(void) {
    try {
        MyClass2 mc2; // MyClass2Exceptionが起こることしか期待しない
    }
    catch(const MyClass2Exception& e) { // 1つ下の例外だけ気にすればいい
        printf(e.what());
    }

    getchar();
    return 0;
}

例外に関する一般論はこのくらいです。最後に例外を使うことでのパフォーマンスの低下について言及します。例外は基本的にパフォーマンスを犠牲にします。特にメモリはオーバーヘッドが生じます。実行速度の面では、throwしない場合はゼロオーバーヘッドです。あってもなくても変わりません。しかし、throwするととたんに遅くなります。(1回のthrowにつき1段階遅くなります)

例外は、throwされることがほとんどないと仮定して考えます。ほとんどの場合、例外が発生する状況では、人間が判断することが多いと思うのでオーバーヘッドは意識しなくてもいいはずです。従って、例外を適切に利用することが、C++でソフトを書く上で必要なことだと思います。

なお、組み込み系ではオーバーヘッドを気にしてtry-catch文を使わないことが多いそうです。

例外のオーバーヘッドについて : http://zakkas783.tumblr.com/post/3870295160/c

0 件のコメント:

コメントを投稿