フィンローダのあっぱれご意見番

第41回:暗黒面の技

初出: C MAGAZINE 1995年9月号
Updated: 1996-02-24

[1つ前] [1つ後] [一覧] [ホームページ]


 通信ソフト「魔王」(mao)は遅々として着々と制作進行中である。削除された発 言のヘッダーも表示するmread機能という、ニフティに喧嘩を売っているような機 能が意外と好評なようで、予想してしたロスタイム活用機能はいまいち反応がな いようだ。

 このソフトの作成中に、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
 二回の演算と一回の比較で済んでいる。しかし、これはいくら何でもやりすぎ に違いない。オプティマイザにやってくれと言うのは酷だが、やってくれると嬉 しいかもしれない。
(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。