2018-01-05

Cプログラマのための、C++簡単裏道アプローチ(5)

増殖するエラー処理は例外処理ですっきりと

想像してみてください。もしも、エラー処理を書かなくて良かったとしたら。コードはどれだけすっきりし、本来の処理の流れが容易に見渡せるのか。
逆に言えば、コードの多くを占めるのがエラー処理ではないでしょうか。特にC言語の場合はそれが顕著です。

そのエラー処理とはいったい何なのでしょうか。まずはエラーの検出。多くの場合、関数の戻り値によって判定します。
エラーと判定された場合、何をするのでしょうか。まずはエラーのレベルの判定をして、致命的で処理を続行することができなければプロセスを終了させなければなりません。ユーザインタフェースを起因とするエラーであれば、ユーザーに通知する必要があります。あるいは、通信相手にエラーになったことを通知することもあるでしょう。
そして、共通するのはあとでメンテナンスをするためにログに書き出すことです。

ログ出力はシステム設計の時点で計画されることが普通で、1カ所にまとめて出力することで処理の分析を容易にすることができます。良くあるのは、エラーレベルという属性を伴うことです。これにより出力時にフィルタリングすることもあれば、ログを解析するときにフィルタリングする場合もあります。よくあるレベル分けには、デバッグ、ウォーニング、エラー、致命的エラー、といった分類で、レベル以外にも種類や系統のフラグ属性を付加することもあるでしょう。大規模なシステムでは、発生すると赤色灯を点灯させるようなものもあります。

一言でエラー処理と言っても、その内容は多種多様です。そして、システムの保守性、信頼性を高めるためにも決して省略することはできません。

しかし、です。このエラー処理のために、プログラムコードから本来の処理の流れが読みにくくなってしまうという大きな問題を抱えています。これは仕方がないのでしょうか。

C++ではCにはなかった例外処理という大胆な仕組みが追加されました。これをうまく使うことで、本来の処理の流れをつかみやすい、良質なコードを書くことが可能になります。

エラー処理の簡単なパターンを示します。
まずはエラー処理を省いて処理の流れを見て、つぎにCでエラー処理を追加した場合にどうなるのか見てください。

コード1

// エラー処理のない仮コード

void func()
{
    foo1();    // どこかのライブラリ関数
    foo2();    // どこかのライブラリ関数
    fooEnd();  // どこかのライブラリ後処理関数
}

void funcRepeat(in n)
{
    for(int i = 0 ; i < n ; ++ i)
    {
        func();
    }
}

int main()
{
    funcRepeat(10);
    return 0;
}


コード2

// エラー処理付き、Cの場合

int func()
{
    int result1 = foo1();   // どこかのライブラリ関数
    if( result1 != FOOLIB_NOERROR )   // libのエラー判定
    {
        // 関数名やコースコード行数とともにログ出力
        logout("foo1() = %d", result1);
        fooEnd();  // どこかのライブラリ後処理関数
        return MYERROR_FATAL;
    }

    int result2 = foo2();   // どこかのライブラリ関数
    if( result2 != FOOLIB_NOERROR )  // libのエラー判定
    {
        // 関数名やコースコード行数とともにログ出力
        logout("foo2() = %d", result2);
        fooEnd();  // どこかのライブラリ後処理関数
        return MYERROR_FATAL;
    }

    fooEnd();  // どこかのライブラリ後処理関数
    return NO_MYERROR;
}

int funcRepeat(in n)
{
    int myResult = NO_MYERROR;

    for(int i = 0 ; i < n ; ++ i)
    {
        if(func(i) == MYERROR_FATAL)
        {
            logout("func() %d回目でエラー", i + 1);
            myResult = MYERROR_FATAL;
            break;
        }
    }

    return myResult;
}

int main()
{
    if(funcRepeat(10) != NO_MYERROR)
    {
        logout("funcRepeat() は失敗した");
        return 1;
    }

    return 0;
}
このコードはもっと最適化することは可能かも知れませんが、現実的にはこのようにエラー処理だらけになってしまいがちです。
では、C++の例外処理ではどうなるでしょうか。

コード3

// エラー処理付き、C++の場合

void func()
{
    try
    {
        foo1();   // どこかのライブラリ関数(例外発生の可能性あり)
        foo2();   // どこかのライブラリ関数(例外発生の可能性あり)
        fooEnd(); // どこかのライブラリ後処理関数(例外は発生しない)
    }
    catch(LibExp &e)   // foo1()またはfoo2() の例外
    {
        fooEnd(); // どこかのライブラリ後処理関数

        // 例外情報を使って内容をログ出力
        logout("%s:%d", e.errstr, e.code);
        throw  MyExp("%sエラー発生!",e.errstr);  // 独自の例外を発生
    }
}

void funcRepeat(in n)
{
    for(int i = 0 ; i < n ; ++ i)
    {
        func(i);
    }
}

int main()
{
    try
    {
        funcRepeat(10);
    }
    catch(MyExp &e)   // 独自の例外
    {
        // 例外情報を使って内容をログ出力
        logout("%s", e.errinfo);
        return 1;
    }
    return 0;
}
tryとcatch、およびthrowというキーワードが現れました。
try{}の中だけを見ると、コード1のエラー処理がない場合とほぼ同じに見えます。正常処理の流れがCの場合に比べ、よく判るのではないでしょうか。
このtry{}の中で例外が発生し、その例外を捕捉するのがcatchです。例外はthrowで発生させますが、このときパラメータを与えます。そのパラメータは自由な型のものが指定できます(たとえばint型でもいいし、独自のクラスや構造体でも構いません)。catchはその型を指定し、関数の仮引数と同じように例外情報を取得することができます。

もう一度コード3を見てください。funcRepeat()の中にはtryもcatchもありません。throwで発生させた例外は捕捉されるまで関数コールを遡って伝わっていくので、ここではmain()で捕捉するようにしています。もし、最後まで例外が捕捉されないと、プロセスは異常終了するはずです。

例外、という言葉は語感として強いものがありますが、どこかで例外が発生したらシステムの処理を続行できなくなる、という意味ではありません。今まで、関数の貴重な戻り値を使ってエラーを通知していたものをそっくり置き換えることが可能です。
たとえば、ファイルのオープンエラーで例外が発生したとすると、再度ユーザにファイル名を入力してもらい、正常処理に戻るといったことも可能です。

また、今回ここでは示しませんが、例外処理は関数コールを跨がなくても使えます。同じ関数の中でtry、throw、catchがあってもいいのです。そう考えると応用範囲はもっと広がるように感じられますよね。

ところで、コード2と3のfuncRepeat()関数を見ておや?と思ったかも知れません。関数コールの途中で何もしなくても良いことを強調するためにこのようにしたのですが、ループの何回目でエラーになったかログを出す必要があれば、次のコード4のようにします。

コード4

void funcRepeat(in n)
{
    for(int i = 0 ; i < n ; ++ i)
    {
        try
        {
            func(i);
        }
        catch(MyExp &e)
        {
            logout("func() %d回目でエラー", i + 1);
            throw;
        }
    }
}
throwにパラメータを渡していません。catchの中でthrowする際にパラメータを指定しなければ、catchした例外を再度発生させることができます。ここではログ出力だけが目的なので、例外情報にはなにも手を加える必要がなく、このようにしました。


さて、ここまで細かいところは気にせず、C++ではこのような例外処理の仕組みを使ってエラー処理を書くことができる、という点だけ押さえておいてください。
例外処理により、エラー処理の冗長なお決まりコードを減らすことが可能です。そのことで本来の処理の流れがつかみやすく、更に、エラー処理自体の流れも判りやすくなり、高品質、かつ、無駄な工数を削減できるコードを書くことが可能になります。

例外処理を使えるだけでも、C++を使う価値があると言えます。これを知っていながらあえてC言語のみを使ってコードを書かなければならないときに感じる理不尽は、かなり強烈なものがあります。

0 件のコメント:

コメントを投稿