#ifndef NDEBUG
if (満たすべき条件) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
#endif
if
文の条件式の部分ね。
assert(満たすべき条件);
デバッグコードはassertでコンパクトになる
assertは、「明言する」や「主張する」などの意味をもつ言葉です。プログラミングでは条件式を明記して、「ここでは、この条件が成立するのだ」と主張します。もし、条件に合わない状況に遭遇したら、それはバグだというわけです。
C言語では、次のようにインクルードを1つ書くだけでassertを使えるようになります。
#include <assert.h>
ここからは、デバッグコードの練習に使ったバブルソートの関数を題材にして、引き続きassertの練習をしていきましょう。関数は次のようなものでした。
/* 整数の配列を小さい順にソートする。
* - pNumbers: ソートする配列の先頭アドレス
* - numberCount: ソートする要素数
* pNumbersが0(NULL)の場合の動作は未定義。
*/
void SortNumbers(int *pNumbers, size_t numberCount) {
/* 引数をチェック */
#ifndef NDEBUG
if (pNumbers) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
#endif
/* バブルソート */
int moves = -1;
while (moves != 0) {
moves = 0;
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] > pNumbers[i]) {
int temp = pNumbers[i - 1];
pNumbers[i - 1] = pNumbers[i];
pNumbers[i] = temp;
moves += 1;
}
}
}
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] <= pNumbers[i]) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
}
#endif
}
/* 引数をチェック */
#ifndef NDEBUG
if (pNumbers) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
#endif
/* 引数をチェック */
#ifndef NDEBUG
assert(pNumbers);
#endif
#ifndef
も要らなくなるのよ。
/* 引数をチェック */
assert(pNumbers);
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] <= pNumbers[i]) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
}
#endif
#ifndef NDEBUG
があると考えてみて。
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
#ifndef NDEBUG
if (pNumbers[i - 1] <= pNumbers[i]) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
#endif
}
#endif
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
assert(pNumbers[i - 1] <= pNumbers[i]);
}
#endif
for
ループは残しておかないとね。
#ifndef NDEBUG
もそのまま残すわけ。
ここまでで、2つのデバッグコードがコンパクトになりました。関数がどうなったか、全体を確認してみましょう。なお、1つ目のassertの前にあった「引数をチェック」というコメントは、見れば分かるので削除しました。
/* 整数の配列を小さい順にソートする。
* - pNumbers: ソートする配列の先頭アドレス
* - numberCount: ソートする要素数
* pNumbersが0(NULL)の場合の動作は未定義。
*/
void SortNumbers(int *pNumbers, size_t numberCount) {
assert(pNumbers);
/* バブルソート */
int moves = -1;
while (moves != 0) {
moves = 0;
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] > pNumbers[i]) {
int temp = pNumbers[i - 1];
pNumbers[i - 1] = pNumbers[i];
pNumbers[i] = temp;
moves += 1;
}
}
}
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
assert(pNumbers[i - 1] <= pNumbers[i]);
}
#endif
}
全体的に、見通しのよいソースコードになったのではないでしょうか。このように、assertを使うとデバッグコードが書きやすくなり、また読みやすくもなります。
ところでassertの正体とは?
assertには、以下の性質があります。
- 条件が成立しないときは、ソースファイル名と行番号を表示して異常終了する
- ただし開発中にだけ有効で、リリース後は無視される
この性質は、次のように書いたassertでも……
assert(満たすべき条件);
または、次のようなassertに置き換える前のデバッグコードでも同じです。
#ifndef NDEBUG
if (満たすべき条件) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
#endif
では、assertの中身は、具体的にどのような仕組みになっているのでしょうか?これは、標準ライブラリの開発元それぞれがassertをどのように実装したかという話なので、決まった答えはありません。
でも、assertと同じものを自作することはできます。ここでは、自分でassertを定義する過程を通して、C言語が標準で備えているassertの仕組みを理解していくことにしましょう。
まず、assertに置き換える前のデバッグコードはお決まりのパターンなので、中身を関数に置き換えることにします。
#ifndef NDEBUG
DoAssert(満たすべき条件, __FILE__, __LINE__);
#endif
ここで、DoAssert()
は自分で次のように作った関数です。呼び出す必要があるのは開発中のみなので、こちらも#ifndef NDEBUG
と#endif
で囲みました。
#ifndef NDEBUG
void DoAssert(bool bCondition, const char *pFile, int line ) {
if (bCondition) {
;
}
else {
printf("NG:%s (%d)\n", pFile, line);
abort();
}
}
#endif
次に、デバッグコードを次のように変形させることを考えましょう。
#ifndef NDEBUG
assert(満たすべき条件);
#endif
これは、関数DoAssert()
の呼び出しに置き換わる、次のようなassert()
マクロを定義すれば可能です。
#ifndef NDEBUG
#define assert(C) DoAssert((C),__FILE__,__LINE__)
#endif
最後に、デバッグコードで#ifndef NDEBUG
を省略できるようにします。
assert(満たすべき条件);
これは、次のようにassert()
マクロを改良すればできますね。開発中はDoAssert()
を呼び出しますが、リリース後は無視されるようにしました。
#ifndef NDEBUG
#define assert(C) DoAssert((C),__FILE__,__LINE__)
#else
#define assert(C)
#endif
というように、assertはマクロとして自作することができます。標準ライブラリに含まれるassertも、おおよそ同じような仕組みだと考えていいでしょう。もちろん、具体的な実装はこれとは異なりますが、マクロでできているという点は同じです。
NDEBUG
が未定義のとき、つまり開発中は、assertの部分が有効なデバッグコードに展開されるわけね。
NDEBUG
が定義されているとき、つまりリリース用にコンパイルしたときは……
assertの力を借りて実装を置き換えてみよう
では、ソートする関数の例に戻りましょう。ここまでで、次の2カ所にデバッグコードを挿入しました。
- 関数が呼ばれた直後:引数をチェックしている
- 関数が終了する直前:ソートの実行結果をチェックしている
つまり、関数の入り口と出口でバグを検出しています。ということは、その間の実装を手直ししたとき、何か間違えてもすぐに気付ける可能性が高いといえます。
では、練習問題をやってみましょう!
今、ソートする関数の中身はバブルソートで実装されています。これを、何か別のアルゴリズムに置き換えてみてください。目標は、次の2点です。
- 今よりも高速なソートにすること
- ソートの結果が、今の実装から変わらないこと
参考として、「マージソート」に置き換える場合の解答例を以下に掲載しておきます。マージソートとは、配列を左右半分ずつに分けてそれぞれをソートしたあと、再び1つの配列に融合(マージ)させる手法です。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
/* 整数の配列を小さい順にソートする。
* - pNumbers: ソートする配列の先頭アドレス
* - numberCount: ソートする要素数
* pNumbersが0(NULL)の場合の動作は未定義。
*/
void SortNumbers(int *pNumbers, size_t numberCount) {
assert(pNumbers);
/* マージソート */
if (numberCount >= 2) {
/* 左半分をソート */
size_t leftNumberCount = numberCount / 2;
SortNumbers(pNumbers, leftNumberCount);
/* 右半分をソート */
size_t righNumberCount = numberCount - leftNumberCount;
SortNumbers(pNumbers + leftNumberCount, righNumberCount);
/* 左右をマージ */
size_t sortedNumberCount = 0;
while ((leftNumberCount > 0) && (righNumberCount > 0)) {
if (pNumbers[sortedNumberCount] <= pNumbers[sortedNumberCount + leftNumberCount]) {
leftNumberCount -= 1;
} else {
int headNumber = pNumbers[sortedNumberCount + leftNumberCount];
for (size_t i=leftNumberCount; i>0; i--) {
pNumbers[sortedNumberCount + i] = pNumbers[sortedNumberCount + i - 1];
}
pNumbers[sortedNumberCount] = headNumber;
righNumberCount -= 1;
}
sortedNumberCount += 1;
}
}
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
assert(pNumbers[i - 1] <= pNumbers[i]);
}
#endif
}
int main(void) {
/* ソートを実行 */
int numbers[] = {56, 0, -34, 2, 976, 2, 5, 55, -20, 48, 16};
size_t count = sizeof(numbers) / sizeof(int);
SortNumbers(numbers, count);
return EXIT_SUCCESS;
}
assertでデバッグの「苦しいところ」を減らそう
assertは、どんどん使ってみるのがおすすめです。はじめのうちは、自分で書いたassertでプログラムが止まってしまうことも多くなるかもしれません。でも、それはプログラムが想定と違う動きをしたということです。つまり、バグが検出されたわけですね。
このとき、「どこから調べればいいのか見当が付かない」ということが起こらないのがassertの特徴です。デバッグのための、あの苦しい時間を減らせるメリットは大きいのではないでしょうか。assertを使い慣れるほど、プログラミングが楽しくなっていくかもしれません!