このソフトの作成中に、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
二回の演算と一回の比較で済んでいる。しかし、これはいくら何でもやりすぎ
に違いない。オプティマイザにやってくれと言うのは酷だが、やってくれると嬉
しいかもしれない。