読者です 読者をやめる 読者になる 読者になる

百日半狂乱

Shut the fuck up and write some code!!

Cのエラーハンドリングと例外設計、例外処理のメモ

C/C++ エラーハンドリング 例外設計 例外処理

二十五日半狂乱、6日目(の分...orz)の記事

Cのエラーハンドリングを毎回やるのは面倒だ!

前回も言ったが、Cではエラーハンドリングに戻り値とerrnoを用いる.

それはそうと例外設計において"無視"は大罪である.

だから、関数を呼び出したら戻り値は漏らさずチェックすべきだ.

ということで、例えば以下のように逐一戻り値をチェックする.

if(send(sockfd, buf, len, 0) < 0){
    ERROR("send");
    exit(1);
}

あぁ、面倒だ.

一体コードのどの部分が正常系の処理なのか?

ほとんどエラーハンドリング*1で埋め尽くされるじゃないか.

そもそもエラーハンドリング部分に書くのは毎回同じコードだし、コードの繰り返しは防ぎたい.

エラー処理部分をラッピングして楽をする

unpv12eの中でラッパーを被せることによってこの面倒を回避する方法を知った.

int
Socket(int family, int type, int protocol)
{
    int    n;
    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

同書では、一貫して上記のように大文字で始まる関数名をラッパー関数として、小文字で始まる同じ名前の関数を呼び出す.

さらに前回記事のエラーメッセージを吐くようにすれば、戻り値のチェックもしなくて良いし、どこでエラーが起こったかもログから一目瞭然になると考えたので、上記のコードを(socket(2)じゃなくてsend(2)だけど)少し拡張した.

send(2)のラッパー関数とマクロ定義

ヘッダに上記のマクロとラッパー関数のプロトタイプ宣言を仕込んでおけば、sendが必要な箇所では単にSend(sockfd, buf, len, 0);と書くだけで良い.

これは楽である!しかも読みやすい!fopenやfgetsなどの関数もラッピング、なんならprintfもラッピングしても良い.

unpv12e(p12)にも、

ラッパー関数の仕様には、closeやlistenなど、普段エラー状態が無視されてしまう関数のエラーを検査できるという、望ましい副作用がある.

とあるではないか.素晴らしい*2

おかげ様でコードは正常系の処理のみを記述したすっきりしたものになった.めでたし、めでたし.

※ただし、エラー発生時に即座にプロセスを終了する場合に限る

正常系、準正常系、異常系

unpv12e(p12)にはさらに以下の様な記述がある.

本書の残りの部分では、明示的なエラーの検査とプロセスを終了させる以外のエラー処理が必要な場合を除いて、これらのラッパー関数を使用する.

単にエラーが起これば終了すれば良いコードを書いているうちは気楽だったが、プロセスを終了させる以外のエラー処理が必要な場合のような状況にぶち当たった瞬間、エラー処理がとんでもなく悩ましいものになってしまった.

上に挙げたsend(2)は、成功した場合、送信されたバイト数を返すが、エラーの場合、-1を返し、errnoを適切に設定する.

プログラマはエラーの原因に応じてプロセスを終了するか、適切なエラー処理を行った上で処理を復帰するか判断しなければならない.

極端なことを言えば、通信途中にコネクションが切れた場合は異常系か?準正常系か?タイムアウトが起こったらそれは異常系か?準正常系か?などをいちいち判断しなければならない.

死ぬべきものを生かしてはならないし、準正常系ならば呼び出し元で対処せよというのは、呼び出し元で中途半端になった処理を巻き戻す処理が必要になったことがある.

そして何が準正常系なのかはアプリケーションの要件次第である.

例えば、サーバプログラムにクライアントからのアクセスが集中しすぎて、fopen(3)やaccept(2)でファイルディスクリプタ不足に陥ったことがある.

そのような状況に陥って初めてそりゃそうかと思ったが、実際にそんな問題が起きるまでは自分の中では特に問題のないコードだった.

こんなしょーもない例だと想像力が足りないと言われてしまいそうだけど、もっと複雑な事象だと予測も難しそうだし、この辺は壮絶に経験が生きそう場面であるような気がする.

手元のアプリケーションでは、コネクションがタイムアウトしてしまう前にファイルを開きたかったので、以下のようにディスクリプタ不足でファイルが開けない場合は、しばらく待って再度ファイルオープンを試みるようにした.

FILE *
fopen_safefd(const char *filename, const char *mode)
{
    FILE *fp = NULL;
    while(1) {
        fp = fopen(filename, mode);
        if(fp != NULL) {
            break; //ファイルオープン成功
        } else if(errno == EMFILE) { //ディスクリプタ不足
            WARNNING("file=%s", filename);
            sleep(1+rand()/((double)RAND_MAX+1.0)); //ディスクリプタが空くのを待つ1+(0~1)秒
            errno = 0;
        } else {
            ERROR("fopen failed:file=%s", filename);
            exit(1);
        }
    }
    return(fp);
}

なんかこうして見ると放置していた問題が山ほどあるような….

・準正常系は呼び出し元で対処せよという規律を無視している.

・待ち時間の1+(0~1)秒という時間に明確な根拠がない.

・ファイルが開けるまでいつまでもオープンを繰り返す問題を持っている.

参考:エラー処理の抽象化#46

まぁ、なんせ設計の段階で準正常系を考慮に入れておかないとAPI設計が妙なことになることがなんとなくわかってきた.

例外設計というのはどうやら想像以上に難しいらしい.

例外処理の話とか、LOG.debug("nice catch!")とか

力尽きたので後は書こうと思っていたことのメモ書き.

発生したエラーが準正常系だった場合、呼び出し元にエラーを伝播させる必要があるが、関数呼び出しがネストした場合にその労力が半端じゃない.

→ もしかして例外処理はその辺から生まれた?

例外は、深い階層で発生したエラーを上位層でハンドリングするために、エラー値を伝搬していくのが手間であるという動機の元に導入された。 エラーハンドリングに関する考え

→ そういえば、「コーディングを支える技術」では、"失敗したらジャンプする"という発想がC言語よりもずっと前の言語からあったという話を起点に、例外処理の歴史をUNIVAC I, COBOL, PL/I,CLU,C++,Windows NT 3.1, D言語, Python, Ruby, JavaScript, Java, C#といった言語の設計思想等も交えて議論されていた.

すごく良いように見えるJavaの検査例外が何故流行らないのかという話が面白い.曰く、「一言で言ってしまえば、面倒くさいからでしょう.」

ちなみにLinuxカーネルでは例外処理(リソース処理)にあたる処理をgoto文で行っているらしい.

→ 例外処理がさまざまな言語で採用される一方でGoogleのコーディング規約ではC++で例外使うなとかいう話もある.

エラー処理の抽象化#26

→ 確かに例外処理が最強であると世の中的に満場一致してないっぽい例としてGo言語のタプルによるエラーハンドリング機構.Goのタプルは言語組み込みかつエラーを無視しにくいらしい.

→ ここまでいくつかスライドのリンクを張ってきたけど、エラーハンドリングと例外設計について凄く参考になった例外とロギング勉強会というものが2012/6/27に行われていたのでメモ.だいぶ有名っぽいけど.

参考:

例外設計における大罪

エラー処理の抽象化

java-ja『LOG.debug("nice catch!")』に参加してきた - Shinya’s Daily Report (最下部に他のブログやスライドのリンクもあります)

java-ja の例外とロギング勉強会で発表してきました - 純粋関数空間

java-jaで例外処理の話をしてきました - 西尾泰和のはてなダイアリー

※例外設計や例外処理の是非については結構意見が分かれるところみたいなので、ちゃんと知りたい場合はリンク先のブログ等も見てみると良いと思います.

その他参考:

例外設計の話。

準正常系と異常系への対応

エラーハンドリングに関する考え

マサカリ投げて下さい.

というわけで、風呂敷広げすぎて後半はもう何が言いたいのかも良くわからなくなったので、タイトルにメモと書いて逃走.

ブログ難しい.

間違ったこと言ってたらマサカリ投げて下さい.

*1:ここで例外処理というと誤解が生じるために、エラーハンドリングとかエラー処理などと表現しているのですが、一方で例外処理という言葉が一般に的な意味で解説されていたりして、本当に日本語って難しいです.

*2:ぶっちゃけ現在もこれで良しとされているのでしょうか?「今時こんな古い書籍のコーディングスタイルを妄信してるなんておめでたい奴だな.」という感じなんでしょうか?などという心配事は尽きないのですが、それはさておき