本物のC

デバッグ:アサーション


コードが大規模になるほどバグの原因を突き止めるのは困難になります。

そこで重要となるのが、コーディングの時点からのバグを目立たせる工夫です。

「バグを目立たせる」とはつまりコードの想定と異なる動作をいち早く検出する事であって、一般にアサーション(assertion)を用いて行われます。

アサーションとは指定した条件式が偽の場合に実行時エラーを出す命令で、C 言語の場合assert.hassertマクロが用意されています。

関数に対して

例えば上のsumの場合、nは要素数なので非負の値でなければなりませんし、dataは非NULLでなければなりません。n == 0 && data == NULLの場合は 0 を返す仕様もありだと思いますが、ここでは

の両方を要求する事にしましょう。即ち、次の様にアサーションを挿入します。

int sum(int n, int *data)
{
int i,
s = 0;
assert(n >= 0);
assert(data != NULL);
for (i = 0; i < n; i++)
{
s += data[i];
}
return s;
}

これにより、例えばdataNULLにして呼び出すと

Assertion failed: data != NULL, file db1.c, line 10

といった文言が出力されて停止します。

これは関数の入力に課す条件(事前条件)ですが、仕様次第では関数の出力、つまり返り値にも何らか条件(事後条件)が課されるでしょう。

制御構造に対して

例えば列挙型account_class

typedef enum account_class
{
ACCOUNT_ADMIN,
ACCOUNT_NORMAL,
} account_class;

と定義され、あるrecord_type(これも何らかの列挙型でしょう)のデータへのアクセス権が

bool is_readable(account_class class, record_type type)
{
if (class == ACCOUNT_ADMIN)
{
...
}
else
{
...
}
...
}

といった関数で判断されているとします。

7 行目からのelse節では、account_classの値が二通りしか無い事を考慮すればclass == ACCOUNT_NORMALは必ず成り立っていなければならない条件(不変条件)です。こうした状況的な

assert(class == ACCOUNT_NORMAL);

を最初に書くべきです。

例えば後に誰かがaccount_classを拡張してACCOUNT_GUESTを追加した場合、実行してみればアサーションの失敗によりis_readableを修正すべき事がすぐ分かるという訳です。

この手のアサーションは

等でも役に立つでしょう。