このソフトの作成中に、NIFTY-Serveの仕様のちょっとした混乱に泣かされてい るわけである。フォーラム、パティオ、INETNEWSの処理を書いた時の話である。 これらの処理は、ほとんどが共通だ。というわけで、微妙に違った所があって、 すごく困る。特に困ったのがパティオである。というのは、mreadした時に、プロ ンプトが出る前に余計な表示が入ってしまうからである。
ともかく、大部分共通の処理で済むのなら、基本的に共通のコードを使おうと いう発想に落ち着く。イメージとしては、LIST 1のようなコードを書くことにな る。
(LIST 1 処理を共有する) switch (type) { case FORUM: case PATIO: case NETNEWS: /* 共通の処理 */ break; }maoは巡回スケジュールをこなした後、スケジュール表を更新する。この処理に 注目してみよう。まずforum,patio,netnewsというキーワードがあるので、これら をファイルに書き込む。その後に各種の情報を書き込むが、これは共通の処理が 使える。問題はキーワードで、共通というわけにいかない。
(LIST 2 キーワードを書き出す) switch (type) { case FORUM: case PATIO: case NETNEWS: if (type == FORUM) { fprintf(fp, "forum"); } else if (type == PATIO) { fprintf(fp, "patio"); } else { fprintf(fp, "netnews"); } /* 共通の処理 */ break; }こんなのはキーワードを配列にすれば一発のようだが、実際は他にいろいろな 処理があるわけで、あくまで説明のための一例と考えていただきたい。ともかく、 LIST 2は場合分けをifにするのがコツである。switchをネストさせると、breakで 一番外に出るという技が使えなくなるからだ。何か判断した結果、処理を中断し なければならない状況になった場合、ifを使って書いておけばbreakでswitchの外 に出ることができる。
だが、あえてswitchを使ってみることにすると、LIST 3のようなコードになる だろう。
(LIST 3 switchの中のswitch) switch (type) { case FORUM: case PATIO: case NETNEWS: switch (type) { case FORUM: fprintf(fp, "forum"); break; case PATIO: fprintf(fp, "patio"); break; case NETNEWS: fprintf(fp, "netnews"); break; } /* 共通の処理 */ break; }まあこれは実に気持ち悪い。直前のswitchでFORUM/PATIO/NETNEWSの判別をして おきながら、また同じような判別をしている。無駄である。もったいないおばけ が出そうだ。ではどうすればよいか。異義を承知で一つ紹介するなら、ここは迷 わずgotoを使いたい。
(LIST 4 gotoを使う) switch (type) { case FORUM: fprintf(fp, "forum"); goto common; case PATIO: fprintf(fp, "patio"); goto common; case NETNEWS: fprintf(fp, "netnews"); common: /* 共通の処理 */ break; }maoも最初はこう書いていた。なお、結局、共通処理を他関数で処理するように 書き直したため、全然違う感じになってしまった。似たような判断をいろんな所 で少しずつ行うことになるのだが、一つの関数のまとまりは良くなるので、可読 性は有利だと判断したのである。
ところで、中には「私は誰が何といおうとgotoが嫌いだ!」という人もいるだ ろう。で、悪魔に誘惑された書き方がないわけでもない。寸前のところで理性が 打ち勝って思いとどまった、という邪悪な手法が、LIST 5のコードである。確か にgotoは消滅している。
(LIST 5 邪悪な技) switch (type) { case FORUM: fprintf(fp, "forum"); if (0) case PATIO: fprintf(fp, "patio"); if (0) case NETNEWS: fprintf(fp, "netnews"); /* 共通の処理 */ break; }恐るべきことに、これは正しいC言語のプログラムだし、fprintfに相当する所 を{}で囲めば、複数の文を書くこともできる。もっとも、インデントには異論が あるかもしれない。通常のルールでインデントを付けるとLIST 6のようになる。
(LIST 6 邪悪な技/インデント修正版) switch (type) { case FORUM: fprintf(fp, "forum"); if (0) case PATIO: fprintf(fp, "patio"); if (0) case NETNEWS: fprintf(fp, "netnews"); /* 共通の処理 */ break; }if文というのは「if (式) 文」という形式だが、文とは何か少し詳しく見てみ ると、大きく分けて6種類あり、名札付き文、複合文、式文、選択文、繰返し文、 分岐文である。この中の「名札付き文」が、奇怪な書き方に使われていることに なる。case名札は、switch文の中以外の場所に現れてはならないという制約があ る。例の書き方だと、switch文のすぐ中にあるわけではないが、中には違いない ようだ。
if (0) の後の文は、原則としてnot reachedのはずだが、ラベルがあってそこ に飛び込んでくるというのが裏技である。オプティマイザはそこまで考えてくれ るだろうか。
*NIFTY-ServeのC言語フォーラム(FC)で、テーブルを使って文字種を判断する話 題があった。これ自体は実にポピュラーなテクニックで、ライブラリ関数のソー スを見れば多分使われているので、ここでは紹介しない。面白かったのは、LIST 7のような式に対しする批評である。 ここでは>=と&&と<=三個の判定が行われてい るというのだが、これをテーブルにすれば判定回数が減って効率的だという。実 はbplやmaoにはLIST 7のような表現がよく使われている。
(LIST 7 数字の判定) (*p >= '0') && (*p <= '9')本来なら標準関数として用意されているisdigitを使うべきだ。不等式で判断した りすると、isdigitの立場がない。そこまで分かっていて、なおこの式を使う理由 は、テーブルを持ちたくないからである。たかが128バイト、あるいは256バイト のテーブルに過ぎないが、bplやmaoというプログラムは、他プログラムから起動 されることが多い。ということは、極力メモリを使いたくないのである。
さて、数字の判定なら、実はこうしなくても、LIST 8のようにすれば、判定は 一度になるのである。
(LIST 8 数字の判定、トリッキー版) (unsigned int) *p - '0' < 10これは、C言語の仕様では、unsignedの引き算が剰余類としての結果になるとい う保証があることを利用した、いくぶんトリッキーな方法である。*pが'0'よりも 小さい値だと、符号付きで計算すると負の数になってしまうのだが、符号無しだ と、とても大きい値になるのだ。プロセッサの命令としても、符号無しの引き算 命令を使うだけの違いがあるだけで、符号無しにすることによって特に処理時間 が増加するという程でもなさそうである。
ただ、こんな書き方をすると、とても分かりにくい。もちろん、多少考えるこ とができれば、これが何を意味するかは、殆どの人が発見できるに違いない。に もかかわらず、一瞥した時の分かりやすさの差は格段である。処理時間を取るか、 分かりやすさを取るか、ということになるが、希望としては、ここまでオプティ マイザがやってくれても罰はあたらないのではないかと思う。
FCでは、実はこの後に、テーブル法ではもっと複雑な判断も一度の表引きで実 行できるという話の流れになっていて、例としてshift JISの1バイト目の判断方 法が紹介されていた。お馴染みの次の式である。
(LIST 9 漢字1バイト目の判定) /* c は unsigned char */ (c >= 0x81 && c <= 0x9f) || (c >= 0xe0 && c <= 0xfc)流石にこれはテーブルを引きたくなりそうだが、ではこれは数字の時のように 一度の比較ではできないだろうか。一見無理のように見えるが、実はLIST 10の手 がある。
(LIST 10 漢字1バイト目の判定、トリッキー版) (unsigned int) (c ^ 0x20) - 0xa1 < 0x3c二回の演算と一回の比較で済んでいる。しかし、これはいくら何でもやりすぎ に違いない。オプティマイザにやってくれと言うのは酷だが、やってくれると嬉 しいかもしれない。