<%@ Register Tagprefix="pt" Namespace="MyControls" Assembly="MyControls" %> <%@ Register tagprefix="pt" tagname="header" src="../header.ascx" %> <%@ Register tagprefix="pt" tagname="footer" src="../footer.ascx" %> <%@ Register tagprefix="pt" tagname="wiki" src="../wiki.ascx" %>

エラー処理 - CooS

この文書はCooSにおけるエラー処理の重要性について述べています。

エラー処理の重要性と煩雑さ

エラー処理はとても大切です。そんなことは普通の人でも分かりますし、当然開発者である私たちも知っています――言葉の上では。

なぜ重要であるかは言うまでもありません。プログラムは動作すれば必ずエラーと遭遇します。遭遇したエラーに対しては、それを修正するか回避するかしなければ間違いなく誤動作の原因となり、いずれプログラムがクラッシュします。(最近では、クラッシュしたら再起動するというエラー処理の手法までありますが。) 私たちは、当たり前ですがクラッシュするプログラムを安心して使うことはできません。逆に、エラーについて正しく対処できるプログラムは、それだけで幾ばくかの安心感を得ることができます。

しかしなぜエラー処理が不完全なプログラムが後を絶たないのでしょうか。次のプログラムを見てください。

if(f(x)==ERROR) error();

エラー処理なんてたった一つの条件文で済んでしまうように思えます。しかし、これは大きな間違いです。

プログラム要素の増加

複数のエラーの区別

先ほどの例は単純すぎます。実際にはエラーの状態が複数あって、それぞれを区別しないといけないときがほとんどです。たとえば、ファイルの処理には、書き込みエラーなのか、ディスクフルなのか、アクセス権限の不足なのかが区別できなければなりません。

そこで、今度は複数のエラーを区別できる例を紹介します。

int ret = f(x); if(ret==ERROR1) error(); if(ret==ERROR2) error();

ふむ、あまり変わったようには思えませんね。しかし、これをたくさんの関数呼び出しごとに行わなければならないことに注意すると、事の重大さが見えてくるはずです。すなわち、関数呼び出しごとに結果を格納するための変数が必要になってしまいます。

ここで、関数呼び出しごとに同じ変数を使い回せばいいと思った方は、要注意です。

変数は、使い回してはいけません。たとえ明らかに大丈夫そうでも、別個の情報を持つ変数は別々にしなければなりません。

今回の場合ですと、ret に格納される情報は、本来であればエラーチェックが終了した時点で終わりです。そしてそのあとは使えなくなるべきです。

ですから、この例で変数を使い回してしまうと、本来は生きていてほしくない情報がプログラムの後々まで生きてしまい、下手すると影響する可能性があるということになります。当然それは好ましくありません。

変数の数はバグの数と無関係ではありません。僕の経験では、扱う変数の数の指数としてバグの発生確率が増加します。変数をむやみに作ることは罪ですが、複数のエラーを区別するために罪を犯してしまっています。

戻り値が使えないときのエラーの受け渡し

今度は、関数の戻り値を他の目的で使うことを考えてみましょう。これもよくあるパターンです。

戻り値以外では主な受け渡しの方法が二つあります。参照渡しの引数を追加してそれに格納する方法と、グローバル変数を用いる方法です。

まず、参照渡しの引数による方法を紹介します。

int f(int x, int* err); void main() { int ret; int y = f(x, &ret); if(ret==ERROR) error(); }

エラーの発生状況は f の実装によって main::ret に格納されることになります。

この方法の問題点は先ほどと一緒で、戻り値を格納するための変数が余分に必要になることです。しかし、汎用性がある方法のためによく使われています。

次は、グローパル変数を用いる方法です。

extern int reason; int f(int x) { reason = ERROR1; return x; } void main() { int y = f(x); if(reason==ERROR) error(); }

これは一見するとうまくいくように思えます。しかし、マルチスレッド環境ではグローバル変数が全く役に立たないために、この方法は意味のないものとなってしまいます。多くのライブラリやOSは当然マルチスレッドですので、この方法は使えません。

Win32 API の方法

最後に、これらを組み合わせた実践的な方法として、Win32 API で多用される方法を紹介します。僕は一時期エラー処理をいろいろ試したことがあるのですが、結局この形が一番無難で、信頼性が高く、かつ使いやすいという結論になりました。

// These below are thread safe. int getErrCode(); void setErrCode(int); bool f(int x) { setErrCode(ERROR1); return false; } void main() { if(!f(x)) { // An error occured int err = getErrCode(); if(err==ERROR1) error(); if(err==ERROR2) error(); } }

この方法のもっとも良い点は、一時的な変数を作ることなく、エラーの判定を if 分の中に組み込めることです。まずエラーが発生したか否かだけを調べることで、エラー処理にかかる部分を完全に分離しています。

可読性の低下

さて、今まで故意に無視していましたが、エラー処理というのは大変に邪魔なものです。邪魔というのは、物理的な意味と精神的な意味があります。

物理的な意味は、皆さんすぐにおわかりになるでしょう。コード量が増えますし、プログラムのメインフロー(もっとも主要なアルゴリズムの流れ)を分断し、可読性を大幅に低下させます。特にエラー処理はプログラムの論理的な構造から外れた処理を要求されることが多いので、たとえば goto を使った方が読みやすかったりします。

精神的な意味というのは、開発者がプログラミングをしているときの気持ちを考えてみてください。

少なくとも僕は、もっとも注力したいのはメインのアルゴリズム部分であり、決してエラー処理ではありません。ですから、難しい処理を書いているときほど、メインのコーディングがうまくいっているときほど、エラー処理なんてくだらないものに時間をとられたくないのです。

エラー処理の本当の問題

僕は、コーディングをしながら常にその次に何が必要になるかを考え続けています。ある関数を書きながら、その関数の動作に必要な次の関数のイメージを固めていくのです。そういった思考の中にエラー処理を細かに考える余地はほとんどありません。ですから、できあがった最初のコードはエラー処理なんて全くないことがほとんどです。(ちなみに趣味の場合だけですので。仕事の場合はエラーチェックして、停止させます。)

そうして、エラー処理に”抜け”が発生します。

エラー処理に抜けが発生した場合、発生したエラーが顕在化するのがかなり遅くなります。それによって「ソフトウェアがエラーを抱えながら動作する」という異常な事態になります。また、エラーが顕在化したときには、そのエラーが最初どこで起こったのか全く分からないということになります。

特にOSなどのシステムソフトウェアにおいてセキュリティに関係した抜けがある場合は、セキュリティホールになってしまいます。つまり、現状のOSにおいては、開発者が注意深くセキュリティチェックを重ねることでセキュリティを確保しています。僕には、とても危うい橋を渡っているようにしか思えません。

このとき、僕(開発者)を責めるのは簡単ですし、そういう風潮はどこにでもあります。しかし、僕はそれは間違っていると思います。

まず、開発者がどんなにがんばっても、エラー処理が100%完全に実施されることは期待できません。勝手に期待して、逆恨みしている人たちはたくさんいるのは悲しいことです。これは今までの歴史から明らかです。

では、どうすれば開発者のパーフェクトを待ち続けることなくエラー処理の抜けに対処できるのでしょうか。簡単です。コンピュータの基本的なソフトウェア――たとえばOSやフレームワークのような――が、エラー処理について基本的な保証をすればよいのです。

例外機構

これらを解決するための手法として、近年「例外機構」が大流行です。

例外機構を使うと、プログラムを次のように記述できます。(Try-catch構文の場合)

void main() { try { f(x); f(x+1); f(x+2); } catch(ERROR1) { // something for ERROR1 } catch(ERROR2) { // something for ERROR2 } }

やっぱりちょっと見にくい気がしますが、今までの書き方に比べてマシなのはいうまでもありません。

また、何より重要な性質として、エラーの抜けが発生しないことが重要です。次のコード例を見てください。

void main() { f(x); g(); }

いままでの(例外機構のない環境)では、g()に到達したときにf()でエラーが起きている可能性がありました。(エラーを調べるためには if 文を組み込む必要がありましたよね。)このままでは、f()のエラーが未チェックになってしまい、抜けが発生してしまいます。

しかし、今度は例外機構があると考えて、上のコード例を見てください。

そのときは、g()が実行されているとき、f()でエラーは起こっていないことが保証できます。もし、f()でエラーが発生し例外が起こった場合には、g()が実行される前に main 関数の外側のエラー処理ハンドラまで実行制御が移ってしまい、g()が実行されることはないからです。

この性質は、「エラーチェックをプログラマが忘れたときでも、エラーが内在化することがない」という大変重要なものです。この性質によって、ソフトウェアをテストをしさえすれば、確実にエラーのチェックを実装できます。

OSへの応用

CooSは、例外機構のこの性質により、エラーの扱いがより慎重になるだろうと予測しています。特に、必要なエラー処理が抜けていた場合でも、きちんとそれを発見することができます。このことは、セキュリティのような誤りが許されないプログラムを含むソフトウェアにおいてとても大切なことです。