ATLAS日本基礎ネットワーク C++トレーニングコース

例外処理

ここでの目標

目次

1.例外処理

一般的に計算機では様々な例外処理を扱っています。ハードウエア割り込みのような例外もあれば、不当命令実行、アクセス違反、ゼロによる割り算、オーバーフローなど、ソフトウエアの実行により発生するものもあります。このような例外が発生すると、制御の流れは現在実行中の一を離れ、例外処理ルーチンへ移ります。多くのハードウエア割り込みの場合は例外処理ルーチンからの復帰後元の処理が継続されます。ソフトウエア実行時の例外はプロセスの処理を強制的にやめたりする場合もあります。こういった枠組みを例外処理と呼びます。

プログラム上にこういった例外処理の仕組みを持ち込むことのメリットを考えてみましょう。例えば次のようなプログラムを見かけます。

so = socket( AF_INET, SOCK_STREAM, 0 );
if( so < 0 ) {
... //エラーメッセージを表示して終了するとか戻るとか
}
st = bind( so, & sin, sizeof( sin ) );
if( st < 0 ) {
... //エラーメッセージを表示して終了するとか戻るとか
}
st = listen( so, 5 );
if( st < 0 ) {
... //エラーメッセージを表示して終了するとか戻るとか
}
so2 = accept( so, & sin, &len );
if( so2 < 0 ) {
.... //エラーメッセージを表示して終了するとか戻るとか
}

いちいち関数呼び出しの結果を確認して、問題があれば処理をする、そうでなければ次に進むという手順を表しています。しかしいまいちスマートでない。もし、これらの関数が例外を発生させてくれれば、

try{
    so = socket( AF_INET, SOCK_STREAM, 0 );
    bind( so, & sin, sizeof( sin ) );
    listen( so, 5 );
    so2 = accept( so, & sin, & len );
}
catch( SocketIOError ) {
    //エラー処理をまとめてここに書く
}

少しスマートになりました。手順の間にif文が挟まらないので流れが読みやすくなりましたし、戻り値で状態を調べるための変数への代入も不必要になります。

このような記述を可能にするためには、それぞれの関数の中でSocketIOErrorというクラスで表される例外を投げる(throw)必要があります。throwの実行によって例外が発生し、どこからであろうと制御はcatch文に渡されることになります。

2.例外を投げる(throw)

例外を投げるにはthrow文を使います。

実習1.

とにかく投げてみましょう。作業場所を用意して

cd ~/tutorial/cplusplus
mkdir l6
cd l6

main.cxxを用意しましょう。

main( ) {
    throw;
}

何もないシンプルなプログラムです。これを実行すると

g++ main.cxx
./a.out
terminate called without an active exception
Aborted (core dumped)

と表示されて実行が終了します。デフォルトのハンドラー(例外処理ルーチン)が呼び出され、その中からabort()が呼ばれます。

実習2.

今度は引数を与えて例外を発生させ、それを受け取ってみましょう。

#include <iostream>

main( ) {
    try {
        throw "Exception!";
    }
    catch( const char * s ) {
        std::cout << "Caught : " << s << std::endl;
    }
}

try節の中で"Exception!"という文字列を引数として例外を投げました。catch側では投げられた例外オブジェクトがconst char *であるので、そこで受け取れます。

g++ main.cxx
./a.out
Caught : Exception!

より詳細な情報をハンドラー(例外処理ルーチン)に渡すためには例外クラスを定義し、その一時的オブジェクトをthrowします。

実習3.

次のファイルMyError.hを用意します。

#ifndef __MYERROR_H
#define __MYERROR_H

class MyError {
public:
    MyError( int error_type ): m_errortype( error_type ) {}
    int getErrorType( ) { return m_errortype; }
    enum ErrorCode {
        INVALID_ARGUMENT, ARGUMENT_MISSING };
protected:
    int m_errortype;
};

#endif

このクラスはメソッドがクラス定義内で実装されています。

これを投げる側では、例えば

if( argc < 2 ) {
    throw MyError( MyError::ARGUMENT_MISSING );
}

等とします。このMyError( MyError::ARGUMENT_MISSING )という表現はMyErrorクラスのコンストラクタの呼び出しですが、これは一時的なオブジェクトの生成を表しています。次と比較してみてください。

if( argc < 2 ) {
    MyError e( MyError::ARGUMENT_MISSING );  //例外オブジェクトをautoで作成
    throw e;    //作成した例外オブジェクトを投げる。
}

この場合、ローカルなメモリーに出来た自動変数eが投げられるのではなくて、そのコピーが一時記憶域に作られます。eは自動変数のため、例外が発生して制御がその関数から離れるときに消滅します。ということですので、この書き方は効率が悪いと言うことになります。

3.例外を受け取る(catch)

MyErrorオブジェクトを受け取りましょう。

実習4.

main関数を書きましょう。argcheck.cxxです。

#include <iostream>
#include "MyError.h"

//引数の数をチェックする関数
void checkArgumentCount( int argc ) {
    if( argc < 2 ) throw MyError( MyError::ARGUMENT_MISSING );
}

main( int argc, char ** argv ) {
    try {
        checkArgumentCount( argc );
        std::cout << "Success." << std::endl;

			    }
    catch( MyError & e ) {
        int errorcode = e.getErrorType( );
        switch( errorcode ) {
        case MyError::INVALID_ARGUMENT:
            std::cerr << "Invalid argument." << std::endl;
            break;
        case MyError::ARGUMENT_MISSING:
            std::cerr << "Argumnet missing." << std::endl;
            break;
        default:
            std::cerr << "Unknown error " << errorcode << " detected" << std::endl;
            break;
        }
    }
}

この中で、メイン関数に引数が割り当てられています。int argcchar ** argvです。コマンドラインに引数を与えることが出来、それをプログラム側から読み取ることが出来ます。この例ではargcだけをチェックしています。このプログラムを実行すると

g++ argcheck.cxx
./a.out
Argument missing.
./a.out test
Success.

プログラムの制御の流れがどうなっているのかわかりますか。

ハンドラールーチンの中でswitch文でいろいろな例外の場合を分類して処理していました。これは必ずしもスマートな実装とは言えません。例外クラスに継承クラスを使うことでよりスマートな実装が可能になります。

実習5.

argcheck2.cxxとして次を作ってみましょう。

#include <iostream>
#include "MyError.h"

class MyArgumentMissing : public MyError {
public :
    MyArgumentMissing( ) : MyError( ARGUMENT_MISSING ) { }
};

void checkArgumentCount( int argc ) {
    if( argc < 2 )
        throw MyArgumentMissing( );
}

main( int argc, char ** argv ) {

    try {
        checkArgumentCount( argc );
        std::cout << "Success." << std::endl;
    }
    catch( MyArgumentMissing & e ) {
        std::cerr << "Argument missing." << std::endl;
    }
    catch( MyError & e ) {
        std::cerr << "Unknown error " << e.getErrorType( ) << " detected." << std::endl;
    }
}

複数のcatch節が用意されている場合、上から順番に型が一致するものを探します。今の場合、MyArgumentMissing型が投げられたのでそのハンドラーに制御が移ります。

g++ argcheck2.cxx
./a.out Argument missing.

試しに、catch( MyArgumentMissing)から}までの3行をコメントアウトして見てください。今度は

./a.out
Unknown error 1 detected.

実際に投げられたエラーはMyArgumentMissing型だったのですが、それはMyError型を継承しているので、その基底クラスが宣言されたハンドラーがcatchしました。

コメントアウトを元に戻し、catch節の順番を入れ替えて試してみてください。先にMyErrorが来て、次にMyArgumentMissingが来ます。今度はコンパイラーが「MyArgumentMissingの例外がMyErrorで処理されてしまいますよ。」と警告してくれます。

ワイルドカード的な、どんな例外も受け取るcatch(...){}というのもあります。

例外ハンドラーの中から、さらに例外を投げることも出来ます。

例外の仕組みをあまり多用すると、プログラムの制御の流れが読みにくくなります。注意が必要です。

4.例外を投げることの宣言

文法的には、例外を投げる場所はソースコードにちりばめられています。そのため、その関数やメソッドの利用者はソースコードを読まないといけないことになります。それを避けるために、関数宣言にその関数がどういう例外を投げうるかと言うことを記述できるようにしてあります。上述の例では

void checkArgumentCount( int argc ) throw ( MyArgumentMissing, MyError ) {...}

というように、関数のエントリーに続けてthrow ( 例外クラス )という形で、その関数が投げうる例外を記述します。必ずしも、このリストにあるものをすべて投げなければならないというわけではありませんが。

次回はテンプレートについて説明します。


前へ 上へ 次へ


2017年7月22日