最近、会社でソースのコメントがらみで苦労して、ふと思った。
適切なコメントの書き方についての実践的な入門資料はないものか?
『CODE COMPLETE 第2版 下 完全なプログラミングを目指して』の第33章や『Code Craft ~エクセレントなコードを書くための実践的技法~』の第5章では良いコメントについて結構なページ数を割いて詳しく論じているし、そこまで詳細でなくてよいのなら『プログラミング作法』の第1章で良いコメントの書き方の基本方針的なものが提示されている。参考になりそうな本はあるのだ。
ただ、ネット上でそれなりにまとまった資料となると、探し方が悪かったからかうまい具合に見つからなかった。本だと買って読んでくれる人が中々いないので*1、ネットで読めるものが欲しいのだ。
そこで、車輪の再発明であることを承知の上で、試しに自分で書いてみた。
対象読者は……そこまで考えていなかった。サンプルコードがC言語で書いてあるので、C言語系のコードが読めるとよいかもしれない。『CODE COMPLETE 第2版 下 完全なプログラミングを目指して』や『Code Craft ~エクセレントなコードを書くための実践的技法~』を持っている人は、このエントリを読む必要はない。
このエントリでは、実際にコメントを書くにあたっての心構え的なものを書こうと思う。気力・体力・時の運が充実して且つ飽きなければ、もう少し実践的に例を挙げるスタイルのエントリをあとで書くかもしれない。
重複を避ける
まず重要なのは重複を避けるということ。DRY(Don't Repeat Yourself)の原則をコメントにも適用するべきだ。
古典的な例だけど、例えばこんなコメントは不要だ*2。
oddflag = input % 2; /* 奇数であるかを判定 */ if (!oddflag) { /* 偶数の場合 */ fprintf(stderr, "奇数ではありません\n"); }
このコメントはコードの内容をトレースしている。しかし何をやっているのかはコードから十分に分かる。だからこのコメントは不要だ。
コードの内容をトレースするコメントが存在するということは、あるアルゴリズムを表現した記述が2重に存在するということだ。片一方を修正した時、もう片一方を修正し忘れると、双方の間に差異が生じる。つまりコードとコメントに矛盾が生じてしまい、後でソースを読む人が混乱する訳だ。
似たような話がある。ソースコードと詳細仕様書だ。同じものを表現しているはずなのに、双方で内容が食い違うことが非常に多い。詳細仕様書の修正にまで手が回らないのだ。
ソースとコメントも同じだ。似たようなものが2重に存在して、両者を同期させる必要があるのなら、メンテナンスの手間は増大する。コードの内容をトレースするコメントは不要どころか害悪になりかねない。
ところで、先ほどのコードが次のような内容だったらどうだろうか?
flg = n % 2; /* 奇数であるかを判定 */ if (!flg) { /* 偶数の場合 */ fprintf(stderr, ERROR_1042); }
こんどはコメントが必要だと思う人もいるかもしれない。「コメントを消したら、何をやっているのか分かりにくくなるじゃないか!」という主張だ。
それは間違っている。いや、間違っているのはコードの方だ。こんな明解さに欠ける変数名や定数名*3を使用しているコードがおかしい。だからコードを修正するべきであり、コードの不備をコメントで誤魔化すのは筋違いというものだ。
付加価値の高いコメントを書く
DRYの原則に基づくとすると、原則としてコメントには「コードに書かれていない/コードからは読み取るのが難しいか読み取るのに時間がかかる内容」を書くことになる。例えば次のようなものだ。
- コードの意図や理由を説明するコメント。
- 意外な要素や記述、特異な部分について注意を促すコメント。但しコメントする前にコードを見直すこと。
- 難しい、込み入った部分の説明コメント。但しコメントする前にコードを見直すこと。
- 要約的なコメント。関数/メソッドの概要及びインターフェイスの仕様や、ある程度まとまった処理を一言にまとめたもの。要約によって抽象化を行う。但しこのコメントはコードとの2重化が発生するので最小限にとどめ、且つしっかりメンテすること。
意図を示すコメントとしては、lint指令が古典的な例ではないかと思う*4。
C/C++/Javaなどにあるswitch文では、breakがらみの失敗がついて回る。breakを書き忘れて大失敗、という奴だ。しかしごく稀にbreakを書かずに落下させたい場合もある。こんなソース*5があったとしよう。
switch (status) { case L_OPERAND: if (!isoperator(*p)) { l_op[i++] = *p; if (i >= MAX_INT_DIGIT) { l_op[i] = '\0'; status = OPERATOR; i = 0; } break; } l_op[i] = '\0'; i = 0; /*FALLTHRU*/ case OPERATOR:
「/*FALLTHRU*/」というコメントがある。Unix環境には昔からlintというC言語のソースコード検査ツールがあるのだけど、このコメントはlintが解釈する。
switch文にbreakが抜けているケースがあったとする。lintがソースコードをチェックする時、breakが抜けていると警告を出す。大抵はbreakを書き忘れているからだ。しかしわざとbreakを書かずに落下させたい場合もlintは警告を出す。breakを書く代わりに「/*FALLTHRU*/」というコメントを書いておくと、lintは「breakを書き忘れたのではなく、わざと書かなかったのだ」と解釈して警告を出さない。
ところで古手のCプログラマにはlintコメントについて知っている人がそれなりにいる。lintを使わなくても、ソースを読むときに「/*FALLTHRU*/」と書いてあれば、落下のテクニックを使っているのだと判断できるのだ。しかし何も書いていないと、breakを書き忘れたのか、それとも落下のテクニックを使用しているのか、俄かには判断できない。
「/*FALLTHRU*/」という短いコメントが、コードを書いた人の意図を明確にしている訳だ。
意図といえば、次のようなコードがあったとしよう。
fgets(buf, sizeof(buf), stdin); …… buf[strlen(buf)-1] = '\0';
ある程度C言語の標準ライブラリで戯れたことがある人なら、このコードの最後の行の意図が分かるはずだ。だから多分、ここにコメントは不要だ。
しかしこれが新人教育用の課題の解答例、つまり新人に見せる為のコードだとしたら、事情は変わる。その場合はコメントで意図を明示する必要があるだろう。
fgets(buf, sizeof(buf), stdin); …… buf[strlen(buf)-1] = '\0'; /* 末尾の改行コードを潰す */
データ/データ構造のコメントを充実させる
手続き型のスタイルでプログラミング入門すると、本人も気づかないうちにロジック中心で物事を考える癖がついてしまうようだ。その為か、コメントについてもロジックに関するコメントに注力してしまう傾向にあると思う。実は初心者がコードの内容をトレースするコメントを書いてしまう理由の一端がそこにあるのではないかと妄想しているのだが、それはさておき。
実装するソフトウェアの性質にもよるけど、大抵の場合ロジックよりもデータやデータ構造がプログラム全体に大きな影響を及ぼす。なので実装の際にはまず適切なデータ構造を考える事が優先される。何だかんだ言っても、プログラマの仕事は情報処理だ。情報=データの取り扱いを慎重に詰める必要がある。
例えば『人月の神話―狼人間を撃つ銀の弾はない (Professional Computing Series)』のP.95にはこんな記述がある。
しかし、データやテーブルの表現をやり直すことで戦略的突破が実現される場合の方がはるかに多い。ここにこそ、プログラムの真髄がある。私にフローチャートだけを見せて、テーブルを見せないとしたら、私はずっと煙に巻かれたままになるだろう。逆にテーブルが見せてもらえるなら、フローチャートはたいてい必要なくなる。それだけで、みんな明白に分かってしまうからだ。
適切なデータ構造が選択されている限り、これは正しいと思う。
私は基本的にモジュール屋さんなプログラマだけど、モジュール設計について先輩から頂いたアドバイスは「データ構造を詰めて考えろ!」だったりする。実際、新人教育の課題プログラムのソースを見ていると、その意味が良く分かる。技術的に優れた先輩が書いた解答例のソースでは適切なデータ構造が選択されていて、可読性や柔軟性が高い。一方で新人が提出した解答のソースは大抵データ構造の扱いがおざなりでロジックでごり押ししていて、その影響で可読性や柔軟性に欠けている側面が見受けられる。データ構造の選択がプログラム全体に大きな影響を及ぼすことがよく分かる。
そんなに大切なものだから、データやデータ構造について解説するコメントを充実させるべきだ。特にデータ構造の雛形となる定義*6やスコープの広い変数*7には原則として説明的で分かりやすい名前をつけた上で、変数名だけでは明確にできない部分をコメントで解説しておくべきだ。関数やメソッドの内部変数では、そこまでする必要は無いだろう――勿論、関数やメソッドが肥大化してきたらスコープの広い変数と似たような扱いにしてもよいかもしれないが、その前にコードのリファクタリングを検討するべきだ*8。
これは別段突飛な発想というわけではない。例えば『プログラム書法 第2版』のP.214にはこんな記述がある。
プログラムに解説をつけるためにも、もっとも効果的な方法の一つは、単にデータの割り付けかたをくわしく説明する、というものである。おもな変数について、その値としてはどんなものが可能かを示し、それが変って行くようすを説明すれば、それだけでプログラムの解説は、ずいぶん進んだといってよい。
もう1つ。『Cプログラミング診断室』の第3章にもこんな記述がある。
フローチャートは禁止しましょう。フローチャートは制御の流れを「もろ」に書けてしまうので良くありません。プログラムは、データを処理するためにあり、データの違いによって制御の流れが変更されます。あくまでも、データが主体です。変数、引数などのデータをどう定義するかで、プログラムの組易さは大幅に改良されます。データ構造がどうなっているかの図の方が、フローチャートよりはるかに役立ちます。データの意味だけはしっかり書きましょう。
http://www.pro.or.jp/~fuji/mybooks/cdiag/cdiag.3.3.html
だいたい、プログラム実行時の処理フローはデータに支配されるものだ。
if (flg) { /* 何らかの処理 */ …… }
このコードで「何らかの処理」が実行されるか否かは、変数flgの値によって決まる。flgが処理フローの決定権を握っているのだ。
だから、データというのは思ったよりも重要なのだ。重要だから名前に気をつけるべきだし、データの振る舞いについての注釈をコメントとして記述する必要がある。
static _Bool verbose_mode = false; /**< verbose modeならtrue */ …… if (verbose_mode) { /* 何らかの処理 */ …… }
この例でのverbose_modeに関するコメントは冗長かもしれない。Unix系のツールを使う人ならverbose modeの意味は説明しなくても分かるはずだ。それにverbose modeなら真になるという挙動は、ブール値では全くもって自然だ。
ただしverbose modeと聞いてピンと来ない人もいる。その場合はverbose modeについて仕様書なりマニュアルなりで明確にした上で、そこで定義した名前を使って補足コメントを書いたほうがよいだろう。
static _Bool verbose_mode = false; /**< 冗長出力モードならtrue */
ステートメントへのコメントは控え目にする
データやデータ構造へのコメントを充実させる一方で、ステートメントへのコメントは最小限にするべきだ。理由は先にDRYの原則と絡めて書いた通り。
特に各ステートメントの内容を逐一反復するようなコメントは最悪だ。あとコメントの中で定数の中身を曝け出していたりなんかすると、眩暈がする。何の為に直値を書かずに定数として定義しているんだ!
#define NORMAL_MODE (1) /* 難易度:標準 */ …… switch (selected) { case NORMAL_MODE: /* 1が選択された */ …… }
ステートメントに関しては、コメントを付ける前に良いコードを目指すべきだ。分かりやすい名前を使い、簡単なロジックで、あまり長くならない程度に済ませる。
理想は「コメントがなくても十分に分かる」だ。もっともそれは難しいので、以下のようなコメントを補足的に追加することになる。
- コードの意図や理由を説明するコメント。
- 意外な要素や記述、特異な部分について注意を促すコメント。
- 難しい、込み入った部分の説明コメント。
あとは要約コメント。ある程度まとまった処理の内容を一言にまとめて記述するものだ。例えば関数内の処理が複数のフェーズに分かれているような時、各フェーズごとにその役割を要約コメントとして記述したりする。関数が十分に短かったり関数内の各要素に分かりやすい適切な名前が付けられていたりする場合、要約コメントは不要だったりする。
2重化の罠に嵌る危険性があるので、要約コメントは慎重に扱うべきだろう。とはいえ、関数やメソッドがどうしても大きくなってしまうような場合、要約コメントは思いがけず役に立つものだ。全てのコードを一から十まで追わなくとも、要約コメントの部分だけ掻い摘んで読むだけで、大雑把な処理の流れが分かる。要約による抽象化のパワーは侮れない。
インターフェイスの仕様をコメントで明記する
関数やメソッドの直前には、その関数/メソッドの役割をコメントとして書く。このコメントのことを、正式な言い回しかどうか分からないが、「関数ヘッダコメント」ということにする。略して「ヘッダコメント」。
関数ヘッダコメントにはステートメントにつける要約コメントと似たような性質がある。つまりある程度まとまった処理の要約が書かれているのだ。その為、暗黙のうちにコードとコメントの2重化が発生する。コードの修正によって2重化の罠に嵌る危険があるのだ。
似たような例でよくあるのが、ある関数のヘッダコメントをコピーして他の関数のヘッダコメントの雛形にしていて、内容の書き直しに漏れが発生したり、そもそもコピペしただけで内容を全く直していない(つまりコピー元のコメントのまま)、といったことだ。見た目の体裁だけは整うが、何の役にも立たない。むしろ百害あって一利なし。
このような危険はあるが、しかしそれでも関数ヘッダコメントを書くべきだ。
関数やメソッドを定義する際に自然なインターフェイスを採用して、且つ関数名/メソッド名や引数名に適切な名前を付けることで関数ヘッダコメントが不要になるように感じるし、それが理想の形だと思うのだが、それらの方針を全ての関数やメソッドに適用するのは非常に難しい。引数として期待する値の範囲やエラー時の処理など、実装のどこかで暗黙の前提が入り込んでしまうものだ。そういった暗黙の仕様を関数名や引数の名前などだけから読み取ることは難しいかほとんど不可能だ。どうしても関数内部のコードを読むことが必要になる。
適切な関数ヘッダコメントが書いてあれば、関数内部の実装を調べる回数が減る。もしかしたら全く調べる必要がないかもしれない。何故なら、ヘッダコメント中に注意事項として暗黙の仕様の内容が書かれているので、コメントを読めば済むからだ。実装を調べなくて済む分だけ時間が浮くし、余分な作業で思考が邪魔されることもない。
勿論、前提としてコード修正と合わせてコメントも修正する必要がある。しかし関数がシンプルなら関数ヘッダコメントもシンプルで済むので、コメントの修正に掛かる時間は十分短い。もし関数ヘッダコメントに大量の記述が必要だとしたら、その関数自体を疑ってみるべきだ。もしくは不要な内容を書いていないか、見直してみるべきだろう。
では具体的に何をコメントすればよいだろうか?
関数ヘッダコメントには、関数/メソッドの概要や役割を1〜2行程度に要約して書くべきだろう。それが最低ラインだ。勿論getterやsetterのように、ごく簡単で且つ名前から中身が十分に分かるものなら、場合によっては省略しても構わないかもしれない。
ごく短い関数や技術的にやさしめな関数では、外部公開インターフェイスでもない限り最低ラインのコメントで十分だ。但し、何か値を戻すのなら、その内容が分かるように要約の書き方を工夫するべきだ。引数がある場合も同様だが、引数に分かりやすい名前が用いられている場合などは、省略しても問題ないこともある。
例えば特定の範囲の乱数を返す関数を書いたとしよう。私なら次のようなヘッダコメントを書く。
/** 0以上max未満の範囲の乱数値を返す */ static int random(int max)
実はこのコメント、基本的に『プログラミング作法』からのパクリなのだが、役割と戻り値が一目で分かるコメントになっている。
やや長めの関数や少し込み入った関数では、関数の概要と引数や戻り値の説明を分けて書く方がよいだろう。複数の引数を取る場合も似たような感じだ*9。
関数やメソッドに何か特筆するべき事柄がある場合は、それも追記しておくべきだ。例えば入力値に制限がある場合はその条件を書くとよいし、エラーが発生した場合の振る舞いを書いておくことも重要だ。何らかのアルゴリズムを元に実装した場合は、元ネタの名前や出典を書いておくと、後で保守担当者が悩まなくて済む。
書く必要がないものもある。前述のrandomのコメントをもう少し詳しく書いたとしよう。次の例には不要な項目が含まれている。
/****************************************************************************** * 関数名 | int random(int max) * * 概要 | 乱数取得関数 * * 引数 | max : 上限値 * * 戻り値 | 乱数値(0以上max未満) * * 詳細 | 0以上max未満の範囲の乱数値を求める * ******************************************************************************/ static int random(int max)
関数名は不要だ。DRYの原則に反してしまう。コメントの直後に関数が定義されていて、そこに関数名などのインターフェイス部分も書かれているのに、ヘッダコメント中に同じものを書く理由が分からない*10。またこの例では概要と詳細の2つの項目があるが、これは1つの項目に統一できそうだ。それに戻り値と詳細で微妙な重複があるのも気になる。
もう一つ、後出しのようだが、このrandomはほんの少し変わった実装をしている。しかしその点についての注意書きが書かれていない。
ということで、私ならこう書く。
/* ====================================================================== */ /** * @brief 0以上max未満の範囲の乱数値を返す * * @param[in] max 範囲の上限+1 * * @return 乱数(整数値) * * @note * 乱数の質を上げる為、特定の範囲の値を捨てている。 * comp.lang.c の C-FAQ 13.16 を参照。 */ /* ====================================================================== */ static int random(int max)
もっともrandomにこのような大層な関数ヘッダコメントをつける必要があるか、という疑問はある。特に制約がないのならば、私なら次のようなコメントで済ませるだろう。
/** * @brief 0以上max未満の範囲の乱数値を返す * @note * 乱数の質を上げる為、特定の範囲の値を捨てている。 * comp.lang.c の C-FAQ 13.16 を参照。 */ static int random(int max)
公開インターフェイスのコメントは詳しめに書く
ところで他人が使うモジュールを実装している場合、外部に公開するインターフェイス部分には詳細な仕様をコメントで書いておくべきだろう。例えばCやC++の場合、ヘッダファイルのコメントを充実させるのだ。利点は次の通り。
- そのモジュールを使う人から自分(モジュール作成者)への問い合わせが減る。
- 別途インターフェイス仕様書を書く手間が省ける。つまりヘッダファイルが仕様書代わり。
ヘッダファイルと仕様書が別々であるよりも、ヘッダファイル中に仕様書を組み込んでしまった方が、メンテナンス性は向上するはずだ。この方法は、『人月の神話―狼人間を撃つ銀の弾はない (Professional Computing Series)』の第15章「もう一つの顔」で言及されている「自己文書プログラム」の考え方に近い。
メンテナンスが楽なスタイルを採用する
最後に、コメントはいずれ保守されるものだという前提で、メンテナンスが楽なスタイルを使うこと。
本来、コメントで重要なのは見た目ではなく正しさだ。とはいえ見た目がグチャグチャなコメントは読みにくいので、それなりに見た目を気にする必要がある。
しかし、それでも「見た目」というのは副次的な要素でしかない。優先すべきなのは正しさだ。そしてコードの修正に伴いコメントも修正しなければ、正しさは保てない。
メンテナンス性が低いコメントのスタイルを採用すると、「コードの修正に伴いコメントも修正」するのが非常に面倒で手間が掛かる作業になってしまう。修正が面倒だと誰もコメントを修正しようとは思わなくなってしまい、結果的にコメントの正しさが損なわれてしまう危険性がある。
例えばコメントブロックをアスタリスクで四角く囲むスタイルでは、コメントを修正する度に右端のアスタリスクの位置を揃えなくてはならないので面倒だ。また行末にコメントを追加するスタイルの場合も、行末コメントの開始位置や終了位置を関数内で統一することに拘りすぎると良くない。コード修正で一部コメントの開始位置が変わった場合などに、他の行末コメントの位置も調整が必要になることがあるからだ*11。
特に一括置換やIDEのリファクタリング機能を使用すると、思わぬところで整形が崩れてしまうものだ。だから最初からあまり整形に注力する必要のないスタイルを使うべきだ。
*1:個人的には「買って読め」と言いたいんだけど……。
*2:個人的にoddflagという名前はちょっと気に入らないけど、ここでは触れないことにする。
*3:この定数名に関しては、特定条件においては認めざるを得ないかもしれないけど。
*4:但し、あまり良い例ではないかもしれない。
*5:operatorという単語を使用しているけど、C++じゃなくてC言語なので勘弁して欲しい。
*6:例えばC言語の構造体/共用体/typedefやC++などでのクラスの定義など。
*7:例えばC言語のグローバル変数やstaticなグローバル変数、C++のメンバ変数や静的メンバ変数など。
*8:単純に関数ないしメソッドの長さで言えば、200行前後が分かりやすさの上限のようだ。『CODE COMPLETE 第2版 上 完全なプログラミングを目指して』のP.214より。
*9:但しCやC++で配列と配列の大きさ(要素数単位)を引数として取るような場合は、少し違うかもしれない。
*10:何らかのツールでコメントから詳細仕様書を生成していて、そのツールの制限で関数名も必要だというのなら理解できるけど。
*11:もっとも変数の宣言や初期化の部分では、行末コメントの方が書きやすいのだが。