C言語プログラミングの覚え書き(改訳)

原文: Notes on Programming in C

Rob Pike

1989年2月21日

Copyright (C) 2003, Lucent Technologies Inc. and others. All Rights Reserved.

Lucent Public License Version 1.02

前書き

KernighanとPlaugerによる“The Elements of Programming Style” (「プログラム書法」木村泉訳)は重要で影響力のある本です。この本にはそれだけの価値があります。しかし、その中の簡潔なルールが、本来意図されたような哲学の簡潔な表現としてではなく、よいスタイルのレシピとして受け取られているように私は時々感じます。この本が変数名は意味を持つようにつけられるべきだと言うなら、名前が使い方を説明するちょっとしたエッセイのようなものであるほうがいいということになるのでしょうか。MaximumValueUntilOverflowmaxval よりもいい名前ということになるのでしょうか。私はそうは思いません。

これから述べるものは、融通の利かないルールではなく、全体としてプログラミングの明確さという哲学を促進するような短いエッセイ集です。すべてに同意してもらおうとは思いません。これらは意見であり、意見は時とともに変わるものだからです。しかし、これらのことは、文書として書いたことはありませんでしたが、私の頭の中に長い時間をかけて蓄積してきたもので、多くの経験に基づいています。そのため、これがプログラムの詳細を計画する方法についての理解の助けになればと願っています(全体を計画する方法についてのよいエッセイを読んだことはありませんが、この文章は一部それについて書いています)。もし変わっていると思われても、問題ありません。同意できないとしても、問題ありません。しかし、なぜ同意できないかを考えていただくきっかけになれば、そちらのほうが望ましいことです。決して、私がこう言ったからこう書くということをしないでください。プログラムの中で達成したいことを最もよく表現できるとあなたが考えるようにプログラムしてください。また、それを一貫して、容赦なく行ってください。

あなたのコメントをお待ちしています。

表示の問題

プログラムは出版物のようなものです。プログラムはプログラマ自身やほかのプログラマ(それは数日後、数週間後、数年後のあなた自身かもしれません)に読まれるもので、そして最後に機械に読まれるためのものです。機械は、プログラムの見た目の美しさを気にしません。プログラムがコンパイルできれば、機械はそれで満足です。しかし、人間は美しさを気にしますし、気にするべきです。時々、やりすぎになることもあります。プリティプリントを行うプログラムは、プログラムのどうでもいい細かいところを強調するようなきれいな出力を自動的に行います。これ文章助詞すべて太字表示するのと同じぐらい馬鹿らしいことです。プログラムはAlgol-68 Reportのような見た目じゃないといけない(システムによってはそのスタイルでプログラムを編集するよう強制したりもします)と考える人は多いですが、明瞭なプログラムはそんな表示をしてもそれ以上明瞭にはなりませんし、ひどいプログラムは笑ってしまうような結果になるだけです。

もちろん、表示についての一貫した規約は見た目をわかりやすくするためには重要なものです。インデントは、最もよく知られた、最も役に立つ例でしょう。しかし、見た目がプログラムの意図より目立つようでは、表示が主体になってしまっていることになり、本末転倒です。ですから、古き良きタイプライター的出力で通すにしても、表示上の馬鹿げたやり方には気をつけましょう。装飾を避けましょう。例えば、コメントは簡潔に、バナーをつけないようにしましょう。言うべきことはプログラムの中で、簡潔に一貫性を持って言いましょう。それから次に進みましょう。

変数名

そう、変数名です。名前で重要なのは、長さではありません。重要なのは表現の明確さです。めったに使われないようなグローバル変数であれば、長い名前をつけてもいいかもしれません。例えば、maxphysaddr のように。ループ内のすべての行で使われるような配列の添字には、i よりも凝った名前は必要ありません。indexelementnumber といった名前をつけるのは、タイプ量が増える(または、エディタの助けを借りる)ことになりますし、計算の詳細よりも目立ってしまいます。変数名が非常に長くなると、何をしているのかわかりにくくなります。これは、表示の問題の一部でもあります。次の二つについて考えてみましょう。

for(i=0 to 100)
    array[i]=0
for(elementnumber=0 to 100)
    array[elementnumber]=0;

実際の例では、問題はもっとあっという間にひどいことになります。添字はただの記法です。そのように扱いましょう。

ポインタもちゃんとした記法が必要です。np が "node pointer" を指しているということが簡単に導けるような命名規約を一貫して使っていれば、npnodepointer と同じぐらい覚えやすいものになります。これについては次のエッセイで詳しく書きます。

プログラムの可読性に関するほかの側面での場合と同じように、命名においても一貫性は重要です。ある変数に maxphysaddr という名前をつけたら、同種の変数に lowestaddress という名前をつけてはいけません。

最後になりますが、私は最短の長さで最大の情報量のある名前をつけ、残りは文脈から補完できるようにしています。例えば、グローバル変数は普通、使用時にあまり文脈がないので、名前は比較的内容がわかりやすいようなものである必要があります。このため、私はグローバル変数には maxphysaddrMaximumPhysicalAddress ではありません)という名前をつけますが、ローカルで定義して使うポインタには NodePointer ではなく np という名前をつけます。これは感覚によるところが大きいですが、感覚は明瞭さに関わってくるものです。

名前に大文字を入れることは避けています。散文調の文章に慣れた私の目には、大文字は不格好で快適に読めません。ひどい表示の仕方と同じように目障りなのです。

ポインタの使用

C はポインタが何でも指せるという点で変わっています。ポインタは切れ味の鋭い道具です。切れ味の鋭い道具というものは、うまく使うと楽しく生産的になりえますが、間違った使い方をするとひどい傷をつけます(この記事を書く数日前に、私は彫刻刀を親指に刺してしまったところです)。ポインタも例外ではありません。ポインタは危険すぎる、何か汚いものだと思われているため、学術界での評判はよくありません。しかし、私はポインタは強力な記法だと考えています。これは、ポインタは明瞭な表現をする役に立つということです。

考えてみてください。あるオブジェクトに対するポインタがあるとき、それはまさにそのオブジェクトに対する名前であって、ほかのものではありません。些細なことのようですが、次の二つの式を見てください。

np
node[i]

一つ目はノードを指していて、二つ目は同じノードを指すように評価されます(ということにします)。しかし、二つ目の形式は式です。あまり単純なものではありません。解釈するには、node が何か、i が何か、そして inode がその周りのプログラムの(おそらく明記されていない)ルールによって関連づけられているということを知る必要があります。式だけを取り出してみると、inode の有効な添字なのかを知る手がかりはありません。もちろん、望む要素を指す添字なのかもわかりません。もし ijk が全部ノードの配列の添字だとすると、簡単にうっかりミスをしてしまいます。その場合、コンパイラは助けてくれません。特に、サブルーチンに渡すときには間違いを犯しやすくなります。ポインタは単純なひとつのものですが、配列と添字は、それがセットとなるものであることを受け取るサブルーチンのほうで信用しないといけません。

オブジェクトとして評価される式は、本質的にそのオブジェクトのアドレスよりもわかりにくく間違いやすいものになります。ポインタは、正しく使うことでコードを単純にできます。例えば、

parent->link[i].type

lp->type 

です。


もし次の要素の type が必要であれば、

parent->link[++i].type

(++lp)->type

になります。

i は値が進みますが、式の残りはそのままです。ポインタの場合、進めるものはひとつしかありません。

ここでも表示の問題が絡んできます。ポインタを使って構造体の中を読み進めるのは、式を使うよりもずっと読みやすいものになります。インクの使用量も減りますし、コンパイラやコンピュータの労力も減ります。関連した問題として、ポインタの型はその正しい使い方と関係しているので、配列の添字と違ってコンパイル時の便利なエラー検出が利用できます。また、オブジェクトが構造体であれば、フィールドは型を思い出す役に立つので、次のようなものは十分に意味がわかります。

np->left

添字によって配列を使う場合、配列はきちんと選んだ名前を持つことになり、式は長くなってしまいます。

node[i].left

ここでもまた、例が大きくなれば余分な文字はどんどん厄介なものになっていきます。

だいたいの場合、もしあなたのコードに似たような複雑な式がたくさんあって、それらがデータ構造の要素として評価されるなら、ポインタを注意深く使うことですっきりさせることができます。次のコードで、

if(goleft)
     p->left=p->right->left;
else
     p->right=p->left->right;

もし p の代わりに複合式を使っていたらどんな見た目になるか考えてみてください。計算の本質を抜き出すには、時には一時変数(ここでは p)やマクロを使うことが役に立ちます。

プロシージャ名

プロシージャ名は、それが何をするかを表しているべきです。関数名は、それが何を返すかを表しているべきです。関数は式の中で使われるもので、if のようなものの中でよく使われます。そのため、適切に読めるようになっている必要があります。

if(checksize(x))

は不親切です。checksize がエラーのときに true を返すのか、エラーでないときに true を返すのかが推測できないからです。それに対して、

if(validsize(x))

はその点を明確にしているので、将来そのルーチンを使うときに間違いが起こりにくいでしょう。

コメント

これはセンスと判断力が必要となる難しい問題です。私は、いくつかの理由から、コメントをあまり書かないようにしています。ひとつは、もしコードが明確で、よい型名や変数名を使っているなら、コード自身が説明になっているはずだからです。それに、コメントはコンパイラにチェックされないので、正しいという保証がないからです。特に、コードが変更されたあとはそうです。ミスリーディングなコメントは非常に紛らわしいものです。最後に、表示の問題です。コメントはコードをごちゃごちゃにしてしまいます。

しかし、私も時々はコメントを書きます。ほとんどの場合、その次に続くことの説明として使っています。例を挙げると、グローバル変数と型の説明(この場合だけは、大きなプログラムでは必ずコメントを書きます)、あまり見ないプロシージャや非常に重要なプロシージャの紹介、大きな計算セクションの区切りなどです。

有名な悪いコメントというものがあります。

i=i+1; /* i に 1 を足す */

そのもっと悪いやり方もあります。

/**********************************
*                                 *
*          i に 1 を足す           *
*                                 *
**********************************/

i=i+1;

笑うのは早いですよ。笑うのは、実生活で出会ってからでも遅くありません。

コメントでは、かっこいい表示を避けましょう。中心となるデータ構造の宣言のような重要なところは例外としてもいいでしょうが(データに対するコメントは、普通アルゴリズムに対するコメントよりもずっと役に立つものです)、コメントの大きな塊を避けましょう。基本的に、コメントを避けましょう。もしコメントがないと理解できないようなら、理解しやすくなるように書き直したほうがいいでしょう。ここで、次の問題が出てきます。

複雑さ

ほとんどのプログラムは複雑すぎます。つまり、問題を効率的に解くのに必要な以上に複雑だということです。なぜでしょうか。多くの場合、それは設計の悪さが原因ですが、その問題は大きすぎるのでここでは飛ばします。しかし、プログラムは細かいレベルでも複雑すぎることが多いもので、そのことについてはここで書くことができます。

ルール1 プログラムがどこで時間を使うかはわかりません。ボトルネックは驚くような場所で起こるので、ボトルネックの場所を証明できるのでなければ、適当な推測で高速化ハックを入れるのはやめましょう。

ルール2 計測しましょう。計測なしに速度のチューニングをしないでください。計測した場合でも、コードの一箇所がほかの場所に比べて圧倒的に時間がかかっているのでなければ、チューニングはやめましょう。

ルール3 かっこいいアルゴリズムは、n が小さいときには遅いものです。そして、n は普通小さいものです。かっこいいアルゴリズムは大きな定数項を持っているものです。n がよく大きな値になるとわかっているのでなければ、かっこいいことをするのはやめましょう(n が大きくなる場合でも、まずルール2 を使いましょう)。例えば、日常業務の問題では、二分木は常にスプレー木より速いものです。

ルール4 かっこいいアルゴリズムは単純なアルゴリズムよりもバグが起こりやすく、実装が難しいものです。単純なアルゴリズムと単純なデータ構造を使いましょう。

ほとんどの実用的なプログラムでは、次に挙げるリストのデータ構造があれば十分です。

  • 配列
  • 連結リスト
  • ハッシュテーブル
  • 二分木

もちろん、これらを組み合わせて複合データ構造を作る覚悟は必要です。例えば、シンボルテーブルは文字の配列の連結リストを含むハッシュテーブルとして実装されるかもしれません。

ルール5 重要なのはデータです。もし正しいデータ構造を選び、物事をうまくまとめれば、アルゴリズムはたいてい自明なものになります。プログラムの中心は、アルゴリズムではなく、データ構造です。(詳しくは「人月の神話」を参照してください)

ルール6 ルール6はありません。

データでプログラムする

アルゴリズムや、アルゴリズムの細かいところは、たくさんの if 文のようなもので書くよりも、データとして書いたほうが、効率よく強力に記号化することができることがよくあります。その理由は、手元の仕事の複雑さが独立した細かい部分の組み合わせによるものであれば、記号化できるからです。典型的な例としては、パージングテーブルというものがあります。プログラミング言語の文法を、定型のかなり単純なコードによって解釈できる形に記号化したものです。この手のやり方としては有限状態機械が特に柔軟に使えますが、何らかの抽象的な入力を「パージング」して何らかの独立した「アクション」列にするようなプログラムであれば、どんなものであってもデータ駆動アルゴリズムにすることでいい結果が得られるでしょう。

このような設計のおそらく最も興味深い側面は、テーブルがほかのプログラムによって生成されることもあるということです。典型的な例でいうと、パーザジェネレータがあります。もう少し身近な例としては、OSがI/Oリクエストを適切なデバイスドライバに割り当てるテーブルによって動いている場合、マシンに接続されたデバイスについての記述を読み込んで対応するテーブルを表示するようなプログラムで「設定」できるでしょう。

データ駆動プログラムが(少なくとも初心者の間で)一般的でない理由のひとつは、Pascalの独裁です。Pascalは、その作者同様、コードとデータを分割することを固く信じています。そのため、(少なくとも元の形では)初期化されたデータを作ることができません。これは、プログラム内蔵方式の原則を定義したチューリングフォン・ノイマンの理論に真っ向から喧嘩を売っています。コードとデータは同じものです。少なくとも、同じにすることができます。そうでなければ、コンパイラがどうして動くのか説明できないでしょう。(関数型言語はI/Oに関して同じような問題を抱えています)

関数ポインタ

Pascalの独裁の結果には、初心者が関数ポインタを使わないということもあります(Pascal では関数を値に持つ変数を使えません)。複雑さを記号化するために関数ポインタを使うことには、いくつかの面白い性質があります。

複雑さの一部は、ポインタの指すルーチンに渡されます。ルーチンは、同じように呼び出されるルーチンセットのひとつとなるので、何かしらの標準プロトコルに従う必要があります。しかし、それより重要なのは、ルーチンが自分の責任範囲のことしかしないということです。複雑さは分散されることになります。

このプロトコルという考え方は、同じような使われ方をする関数は同じようなふるまいをしなければならないというものです。このことが、ドキュメントの記述やテスト、プログラムの発展をやりやすくしています。さらには、プログラムをネットワーク越しに動かすこともやりやすくなります。プロトコルは、リモートプロシージャコールとしても記号化できるのです。

私の主張は、オブジェクト指向プログラミングの核心となるものは、関数ポインタを明確に使うことだというものです。データについて実行したい操作セットがあり、それらの操作に対して適用したいデータ型のセットがあるなら、プログラムをまとめる一番簡単な方法は、それぞれの型に対して関数ポインタのグループを使うというものです。これは、一言で言うと、クラスとメソッドを定義するということです。もちろん、オブジェクト指向言語にはそれ以上のものがあります。きれいな構文、派生型など。しかし、概念的には、ほとんど変わるところはありません。

データ駆動プログラムと関数ポインタを組み合わせると、びっくりするほど表現力のある書き方ができるようになり、私の経験では、意外な喜びをもたらしてくれることもよくありました。特別なオブジェクト指向言語なしでも、余計な手間なしでオブジェクト指向のいいところの90%を手に入れることができ、結果についても自分でコントロールしやすくなります。これほどお勧めできる実装スタイルはありません。この方法で構築してきたプログラムは、かなりの開発を重ねてもうまく生き残っています。もっとゆるいやり方の場合よりも、ずっといい結果です。きっと、この手法に求められる規律は、長い目で見ると割に合うということでしょう。

インクルードファイル

単純なルールです。インクルードファイルはインクルードファイルを決してインクルードしてはいけません。その代わりに、どのファイルを先にインクルードするべきかが(コメントで、または暗黙的に)言明されていれば、どのファイルをインクルードするべきかという問題はユーザ(プログラマ)に押しつけられますが、ある意味では扱いやすく、組み立て方によって多重インクルードを避けることができるようになります。多重インクルードはシステムプログラミングの癌です。ひとつの C のソースファイルをコンパイルするのに、5 回以上もインクルードされるファイルがあることは珍しいことではありません。この面から言うと、Unix/usr/include/sys はひどいものです。

#ifdef を使ってファイルが 2 回読まれないようにする小手先のテクニックがありますが、実際には正しく運用されないものです。#ifdef はファイル自身の中にあり、インクルードする側にあるのではありません。結果として、何千行もの不要なコードが字句解析器に渡されることになってしまいます。これは、(良いコンパイラの場合)最も負荷の高いフェーズになってしまいます。

単純なルールに従いましょう。