バグを生まないコーディング法、10個の規則でソフト開発を効率化
ソフトウエア開発にはバグがつきものだ。ただし、バグの発生を最小限に食い止める方法がある。コーディング規則を適用してコードを記述することだ。バグが発生してからそれを発見し、修正するという通常の開発手順に比べて、簡単に、しかもコストをかけずにバグをつぶせる。
ここでは、ZigBeeを利用したセキュリティ・システムから医療機器にわたる筆者の組み込みソフトウエア開発の経験から得た、バグをなるべく発生させないコーディング規則を紹介する(「続・バグを生まないコーディング法」はこちら)。
なぜコーディング規則が必要か
コーディング規則は、ソフトウエア開発者に対して、コードを記述する上での規則をまとめたものである。英語のライティング教本として著名な「The Elements of Style」(William Strunk Jr.、E. B. White著)の、プログラミング言語版のようなものだ。
組み込みソフトウエアにも、きれいで、正しく、簡潔に書くための規則が定められているべきである。ソフトウエアの開発を手掛けるチームや企業の単位でそのようなコーディング規則を適用すれば、多くの利益が得られる。例えば、ソフトウエアの信頼性や移植性を高められ、最小限の手間をかけるだけでそのソフトウエアを再利用できるようになる。さらに、あるソフトウエアの開発に携わる各メンバーに対して、個別にコードの内容をレビューしたり説明したりするのに必要な時間を減らせる。
このように、コーディング規則を適用することで得られる数ある利益の中でも、最も大きなメリットはバグを減らせることだ。適切にコーディング規則を適用してソフトウエア開発を進めることで、簡単に、しかもコストをかけずにコードの中に忍び込むバグを食い止められる。つまり、ソフトウエアの開発コストを下げるための重要な戦略は、コンパイラやリンカー、ソース・コードの静的解析ツールなどが自動的にバグを排除できるようにコードを記述すること、言い換えるとコードを実行する前にバグをなるべく発生させないことである。
もちろん、ソフトウエアにバグが発生する原因はいくつもある。最初に開発を手掛けたソフトウエア開発者が記述したソース・コードの中に、実はバグが入ってしまっていたにもかかわらず、長い間気付かれず、数カ月、あるいは数年後に見つかることもある。ソース・コードの記述内容があいまいで、あるソフトウエアの完成後に、別の開発者が機能を拡張したり、移植したり、一部のコードをほかのソフトウエアに再利用したりする際、コードに対する誤解が原因で新たなバグが発生することもある。
新規ソフトウエアを開発するに当たってコードを記述するときに入り込んでしまうバグについては、その数や重大性を、適切なテスト工程を通すことで減らせる。ただし、新規ソフトウエアの開発を担当したソフトウエア開発者のコード記述作法によっては、ほかのソフトウエア開発者がそのコードをメンテナンスするときに、バグを誘発してしまうこともある。例えば、「int32_t」のような固定長の整数型を適切に使っていれば、利用するコンパイラを変えたり、ターゲットになるプロセッサを変えたりしても、そのために必要な移植作業を施したコードが予期せぬオーバーフローを起こすことはない。ところが、C言語が潜在的に持っているあいまいな点などが原因で、コードの書き方によっては予期せぬ振る舞いを示すことがある。
メンテナンス担当のソフトウエア開発者が引き起こしてしまうバグについては、その数と重大性を、一貫性のあるコメントを記述することなどによって減らせる。つまり、ソース・コード中に適切にコメントを書いておけば、そのコードに関わるすべてのソフトウエア開発者が、ソフトウエアを構成する変数や関数、モジュールの正しい使い方や意味を容易に理解できるからだ。
複数あるコーディング規則
C言語に向けたコーディング規則はいくつもある。例えば、欧州の自動車業界団体のMISRA(Motor Industry Software Reliability Association)が発表した自動車用ソフトウエアの開発にC言語を利用する際のガイドライン「Guidelines for the Use of the C Language in Critical Systems(MISRA-C:2004)」がある。MISRA-Cの策定者は高い安全性が求められる機器の設計に関する見識があり、このガイドラインは、C言語でより安全なソフトウエアを記述するための規定となっている。
コーディング規則とMISRA-Cには共通点があるものの、違いもある。コーディング規則は主に、記述方法に重点が置かれている。それに対してMISRA-Cは、バグを発生させないための重要なルールを規定している。
そのため筆者が所属するチームでは、必要に迫られて、独自のコーディング規則を開発した。このコーディング規則は、バグを生まないコードを書くという観点で、まったく新たに作ったものだ。
バグを生まないためのルール
以下に、このコーディング規則のいくつかの例を紹介する。これらのルールは、バグの発生件数の削減に役立つだろう。
●ルール1
if文、else句、switch文、while文、do文、for文に続くコード・ブロックを、常に中括弧「{ }」でくくる。これらの文や句に続くコードが1文だったり、何もなかったりした場合でも、中括弧でくくるべきである(図1)。
理由は、次の通りである。例えば、if文に記述した条件が成立したときに処理すべき内容が、当初は「A」という1文で記述できていたとしても、その後改変を加えて「A」と「B」の2文になったとする。このとき、最初の「A」を中括弧でくくっていなければ、「B」を追加すると同時に中括弧の記述を忘れると、後から加えた「B」という処理が、if文の条件が成立するか否かにかかわらず常に実行されてしまう。つまり、新たなバグを生み出してしまうことにつながる。このようなたぐいのバグは、常にコード・ブロックを中括弧でくくるようにしておけば、未然に防げる。

if文、else句、switch文、while文、do文、for文に続くコード・ブロックを常に中括弧でくくる。
●ルール2
次の条件に当てはまる場合は、常にconst修飾子を使って宣言する。
- 変数の初期化後、その値を変更することがないならば、その変数を宣言するときにconst修飾子を付ける。
- 関数に値を参照(ポインタ)で渡し(call-by-reference)、その値を関数内で変更することがないならば、その関数の宣言時にその引数にconst修飾子を付ける。例「char const *p_data」。
- 構造体や共用体のメンバーのうち、変更できないメンバーにはconst修飾子を付ける。例えば、メモリー・マップトI/O方式を採る周辺機器のレジスタにアクセスするための構造体メンバーなどである。
- #defineで定義する数値定数のデータ型を厳密にする場合、const修飾子を使って宣言する。
これらの理由は、コンパイラの機能を使ってデータをリード・オンリーにして、意図しない書き込みからデータを保護するためである。
●ルール3
関数や変数を、それが定義されているモジュールの外に公開する必要がない場合は、staticキーワードを使って宣言する。
理由は次の通りだ。C言語のstaticキーワードにはいろいろな意味がある。モジュール・レベルでは、グローバル変数や関数を、staticキーワードを付けて宣言すれば、ほかのモジュール内の関数による不用意なアクセスから保護できる。このようなstaticキーワードの使い方は、カプセル化にもつながる。
●ルール4
次の条件に当てはまる場合は、常にvolatile修飾子を使って宣言する。
- 任意の割り込み処理ルーチンからアクセスされるグローバル変数を宣言するとき。
- 2つ以上のタスクからアクセスされるグローバル変数を宣言するとき。
- メモリー・マップトI/O方式を採る周辺機器のレジスタ・セットにアクセスするためのポインタ変数を宣言するとき。例「timer_t volatile const *p_timer」。
理由は次の通りである。コンパイラは、並列実行される複数のスレッドによって変更される可能性のある変数やレジスタであっても、それらに対する読み出し/書き込みコードが不要だと判断するとコード自体を削除するように最適化する場合がある。
例えば次のような場合だ。あるスレッド「A」が、ある条件のときに変数の値を変更するとしよう。別のスレッド「B」はその変数をポーリングしていて、値が変更されたときに何らかの処理をする。このとき、スレッドBの中に変数の値を変更するようなコードが書かれていなければ、コンパイラはその変数の値は変更されないと判断してしまって最適化する場合がある。すると、コンパイル後のオブジェクト・コードでは、変数をポーリングするコードが削除されてしまったりする。volatile修飾子を適切に使うと、このようなコンパイラの最適化機能が無効となり、コード全体にわたるような発見が難しいバグの排除につながる。
●ルール5
コメントをネストさせない。そして、たとえ一時的であっても、コード・ブロックを無効にする用途(コメント・アウト)にコメントを使わない。一時的にコード・ブロックを無効にするには、プリプロセッサの条件コンパイル機能を使う(図2)。例「#if 0 ... #endif」。
理由は、ネストしたコメントを使ったり、コメントでコード・ブロックを無効にしたりすると、コンパイルして生成される最終的な実行モジュールの中に、無効にしたはずのコードが入り込んでしまうリスクが生じるからだ。

ネストさせない。コード・ブロックを無効にする(コメント・アウト)ために利用しない。
●ルール6
整数値のビット長やバイト長が問題になるときは、charやshort、int、long、long longといったデータ型を使わず、固定長データ型を利用する。C言語の標準で規定されている符号付きおよび符号なしの固定長整数型は、表1の通りである。
理由は次の通りだ。ISOが策定したC言語標準「ISO/IEC 9899」では、charやshort、int、long、long longといったデータ型の長さ(ビット長)について、処理系ごとに定義することが許されており、移植性の問題を引き起こす。1999年に改定された「ISO/IEC 9899:1999」でもこの問題の潜在的な原因は解決されていないが、表1に示したビット長が一意に定義されているデータ型が新たに追加された。これらの新しいデータ型は、「stdint.h」というヘッダー・ファイルで定義されている。

●ルール7
「&(論理積)」、「|(論理和)」、「~(ビット反転)」、「^(排他的論理和)」、「<<(左シフト)」、「>>(右シフト)」といったビット演算子を、符号付き整数値に適用しない(図3)。
理由は、C言語標準が、例えば「2の補数を使う」というように、符号付き整数型データの表現形式を規定していないことにある。符号付き整数値に対してビット演算を実施した結果については、処理系に依存しているのが現状だ。

符号付き整数値に適用しない。
●ルール8
符号付き整数と符号なし整数を組み合わせたり、比較したりしてはいけない。この原則を守るために、符号なし10進定数(即値)の末尾には「u」を記述する(図4)。
理由は次の通りである。符号付き整数型変数に格納されたバイナリ・データの取り扱いの詳細については、C言語標準では処理系に依存するとされている。しかも、符号付き整数と符号なし整数を組み合わせた結果引き起こされるバグは、実際に演算するデータの内容に依存するため、発見が難しい。

符号付き整数と符号なし整数を組み合わせたり、比較したりしてはいけない。
●ルール9
インライン関数を利用して記述できる内容ならば、パラメータ付きマクロを使わない(図5)。
理由は次の通りだ。プリプロセッサ命令の#defineを利用することにはリスクが伴う。特にパラメータ付きマクロに伴うリスクは大きい。図5に示した例文のように括弧が広範囲に及ぶことが問題で、「MAX(i++, j++)」のような呼び出しに対して、意図しない2重のインクリメントが実行される可能性を排除できない。
ほかにも、符号付きデータと符号なしデータの比較といった、マクロの間違った使用法に対するリスクがある。マクロの場合、関数と異なり、コンパイラによる引数のデータ型チェックが働かない。なお、「inline」はもともとC++言語のキーワードだったが、「ISO/IEC 9899:1999」でC言語標準にも追加された。

なるべくインライン関数で記述する。
●ルール10
変数を宣言するときに、「,(コンマ)」を使わない(図6)。
理由は次の通りである。変数宣言を、それぞれ独立した文で記述する手間は少ない。それに対して、コンパイラや、コードのメンテナンス担当者が、図6のような記述が意図する内容を誤解してしまうリスクは高い。

複数の変数を宣言するときに「コンマ(,)」でつないで一度に記述しない。
PR











