Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第83回「iの主張」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

「引用禁止問題」というのをご存知だろうか。 FPROGでもよく盛りあがる話題だ。 これは、発言の最後に「私の発言を引用することを禁止します」と付記されている場合に、 それを引用する人がいると発生するのである。 そもそも、私の経験では、引用を禁止するような人を相手にしてまともに会話になった覚えがない。 必ず訳のわからない不毛な結果になる。 だから、そういう人は相手にしないことにしている。 読んで無視するだけでも面倒だから、 該当者の発言は読む前にログから削除する。

まともな会話にならないのには、理由がある。 引用を禁止する理由を考えてみれば分かるはずだ。 あえて説明しないので、想像してみていただきたい。

ところで、引用を禁止されている発言を引用したらどんな法的問題が発生するのだろうか。 引用という行為は著作権法で認められた権利である。 即ち、引用を禁止する側が権利の濫用なのであり、 そういう宣言は無効だ。 と今まで思っていたのだが、 某フォーラムで、 引用を禁止するという宣言自体は、 民法第一条にある「私権の行使」であって法的な効力を持つ、 というコメントが発表されたのである。 これは面白い。

ちなみに、民法第一条というのは、こういう内容だ。

第一条 私権ハ公共ノ福祉ニ遵フ
2 権利ノ行使及ヒ義務ノ履行ハ信義ニ従ヒ誠実ニ之ヲ為スコトヲ要ス
3 権利ノ濫用ハ之ヲ許サス

私見ながら、これは公共の福祉、信義誠実の原則、 権利濫用の禁止という条件を守らないと私権を行使できない、 という制限的なものだと思っていたから、 著作権法で万人に認めている権利でも、 個人の意志で勝手に制限しても構わない、 という解釈には心底驚いたわけであるが、 実際そういう主張で運営されているフォーラムもニフティサーブには現に実在するわけだから、 世の中広い。

§

とりあえずその話題はさておき、FPROGORGで盛りあがった話題を紹介する。

List 1 のような書き方がある。

---- List 1 ----

    if ((fp = fopen(filename, mode)) != NULL) {
        /* 何か処理 */
    }

---- List 1 end ----

この書き方自体はポピュラーなものだから、 この通り書いても別段かまわないが、 私は過去に何度か、 このような場合はList 2 のように書いた方がよいと主張している。 この方が処理が明快になる、というのがその理由である。

---- List 2 ----

    fp = fopen(filename, mode);
    if (fp != NULL) {
        /* 何か処理 */
    }

---- List 2 end ----

ところで、List 3のような書き方がある。 しばしばwhileループの中の処理として、 何等かの方法で得られた文字をどんどんarrayという配列にセットしていく、 というような処理に使われる。

---- List 3 ----

    array[i++] = c;

---- List 3 end ----

List 1よりList 2がよいと主張するのなら、 List 3の処理はList 4のように書いた方がよいのではないか、というご意見があった。

---- List 4 ----

    array[i] = c;
    i++;

---- List 4 end ----

ところが、私のプログラムでは、 List 3のような場合にList 4のように分けて書くことはない。 今まで深く考えずにそうしていたのだ。はて、なぜだろうか?

いずれの場合も、分けて書けるということは、 当然ながら、複数の処理が含まれているということになる。 List 1の場合「fopenを呼出してファイルポインタを得る」という処理と、 「得たfpの値を確認する」という処理がそれに相当する。 List 3の場合「array[i]にcの値を入れる」という処理と、 「iの値を1進める」という処理である。

まず一般論として、複数の処理が必要な場合、 それらを分離して既述することが望ましいのか。 必ずしもそうとはいえない。 処理を分解しすぎると、 かえって各処理の関連付けが見えにくくなり、 理解の妨げとなることがあるからだ。 何事も限度というものがあるのだ。 処理単位毎に関数を作って呼出せば、 各関数はすっきりしても、 この関数が全体の処理の中で何をしているのか分からない、 という状況になることがあるが、それと同じだ。

というわけで、 ポイントは処理の関連性にありそうだ。 では、今回の例は処理の関連性にどのような差があるか。

fp の値を得るということと、その値がNULLでないことを確認するという処理、 この二つの関連性は弱い。 なぜなら、 List 1のように書ける所をList 2のように書いても、 何か理解が難しくなるような要素は見当たらないからである。

これに対して、array[i++] = c; という書き方は、 解釈の段階において、かなり違った読み方をしているのではないか。 プログラマーズフォーラムでこの話題が出た時、 この処理を「インデックスがオートインクリメントするarrayという名のバッファにcを入れる」と説明した。 arrayというバッファを想定し、 1文字突っ込んだら、次の文字を突っ込む位置を一つずらす、 という処理を一連のものとして、即座に動作させたいのである。 そのためには、array[i++] = c; という書法はかなりユニークな主張を表現したものだと思える。 もちろん、 他でiの値を変化させないという大前提は必要だが、 この書き方だと、値を追加した瞬間に、 次に値が入る位置が「次の場所」になっていることが保証されるのである。

§

これに対する反論として、List 5、List 6 のような例が紹介された。 これらは「関数呼び出し + 代入 + 比較」のパターンとして、 List 1の例と類似しているというご指摘があった。言われてみればまさにその通りだ。

---- List 5 ----

  while ((c = getchar()) != EOF)

---- List 5 end ----
---- List 6 ----

    if ((c = fgetc(fp)) == EOF)
        break;

---- List 6 ----

これらのパターンは「よく見かける」ものであって、 その意味において、別段問題ないのではないか、というのである。 見なれたものは理解しやすい、 というのもセオリーである。 確かにパターンという発想はうかつだった。 それに、C言語の「式が値を持つ」という特徴を生かそうとすれば、 このような定石が生まれるのは当たり前のことだ。

個別に考えてみよう。List 5 のような書き方は、 簡単に分離することがそもそも不可能である。 while文の条件判定部にはブロックを書くことができないからだ。 無理に分離しようとすると、List 7 のようになってしまう。

---- List 7 ----

  while (1) {
        c = getchar();
        if (c == EOF)
            break;
    }

---- List 7 end ----

これでは、どういう条件の時にループする、 あるいは「しない」のかが、かえって分かりにくくなってしまう。 この場合は List 5の書き方がよろしい。 しかし、List 6を分けるとList 8 のようになる。 これはList 8の方がよろしい。

---- List 8 ----

    c = fgetc(fp);
    if (c == EOF)
        break;

---- List 8 ----

List 8は 「fgetcで得た値をcに突っ込む」という処理と、 「それをEOFと比較する」という処理の関連度に注目する。 これらの処理に、何となく間があるというか、 ワンクッションあるというか、 隣の部屋に移動するためにドアを明けて通るという感じの区切りを感じるのだ。 fgetcを実行して「から」、EOFと比較すればよいのである。

これに対して、buf[i++] = c;、という処理は、 buf[i]にcを代入するという処理と、 iを1つ増やすという処理、 これらは滞りなく行われなければならないのである。 つまり、これは複合した処理であると同時に、 感覚としては1つの処理とみなしているような気がする。 そうなってしまうと、この処理はそもそも二つに分けるという発想が出てこないのである。

§

ところで、List 3の処理は、もともとList 9のような処理の一部であった。

---- List 9 ----

    for (i = 0; i < N; i++) {
        c = getc():
        if (c == EOF)
            break;
        array[i] = c;
        if (c == '\n') {
            i++;
            break;
        }
    }

---- List 9 end ----

これは、arrayというバッファにgetcで1文字ずつ代入し、 '\n'が来るかEOFでループ終了、というものである。ここで問題になるのがiの値である。 特に、cが'\n'である時に、iを増加させてからbreakしている。 なぜここでiを増やしてやらなければならないか。 何も考えずにこのことを承認できるだろうか。 重要なポイントである。

この問題に関してなら、List 10のような書き方をすれば、iの値に確実性が生まれる。

---- List 10 ----

    i = 0;
    while (i < N) {
        c = getc():
        if (c == EOF)
            break;
        array[i++] = c;
        if (c == '\n')
            break;
    }

---- List 10 end ----

すなわち、iの値は、バッファに文字を追加しなければ変化しない。 しかも、1文字追加した場合には、必ずそれが追加した文字の次の位置に移動しているから、 iが指している先が配列の最後(の次)である場合を除いて、 そこが次に書き込む位置であることが任意の時点で保証されているわけである。

ところが、List 9は、iをバッファの中の位置ではなく、 むしろループカウンタとして利用している。 iが「forのループを制御する式内」でだけ変化する場合には極めて分かりやすいという意味で正攻法なのだが、 問題は、cが'\n'の時だけbreakする前に値を変えることである。 i++を一箇所に集めるのは簡単で、 List 10のi++の個所を最初に紹介したように2つの文に分ければよいのである。 しかし、その結果は、やはり 「ループカウンタとしてのi」という発想とは若干ずれた内容にならざるを得ない。 だったらiをインデックスと割り切ったList 4の書き方は、 それなりに高い評価としてよいのではないか。 というのが私の結論なのだが、皆さんのご意見はいかがだろうか。

  

(C MAGAZINE 1999年5月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-15 裏ページに転載。

(C) Phinloda 1999-2006, All rights reserved.