デバッグコードを書いてみよう

デバッグコードはバグを検出するための仕組みだと分かったところで、実戦でも使える書き方を練習しましょう。ここまで例として使ってきた次の関数を、今回も使うことにします。

/* 整数の配列を小さい順にソートする。
 * - pNumbers: ソートする配列の先頭アドレス
 * - numberCount: ソートする要素数
 * pNumbersが0(NULL)の場合の動作は未定義。
 */
void SortNumbers(int *pNumbers, size_t numberCount) {

  /* ここに、ソートの実装 */

}
んんー、これだけだと、あんまり雰囲気がでないかしらね……
はーい、提案です!実際にソートが動く関数を使ったら、練習になると思います!
そうね。そうしましょう。ちょっと待っててね……
(カタカタカタ……)
/* 整数の配列を小さい順にソートする。
 * - pNumbers: ソートする配列の先頭アドレス
 * - numberCount: ソートする要素数
 * pNumbersが0(NULL)の場合の動作は未定義。
 */
void SortNumbers(int *pNumbers, size_t numberCount) {

  /* バブルソート */
  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;
      }
    }
  }

}
関数の中身を、シンプルな「バブルソート」で実装してみたわ。
早っ!バブルソートって、シンプルなソートなんですか?
そうね。動作は遅いけど、アルゴリズムが分かりやすいソートなのよ。

バブルソートは、先頭から順に隣り合う値を比較して、大小関係が逆だったら入れ替えていくソートです。この操作を繰り返し、入れ替えが1度も発生しなくなればソート完了です。値がフワフワと少しずつ移動していく様子が泡のようなので、「バブルソート」と呼ばれています。

ソートのアルゴリズムは今回の主題ではないので詳しい説明は省きますが、上の例は実際に動作するC言語の関数です。ぜひ、練習に使ってみてください。

バグが目立つようにする

じゃあ、始めるわよ。
お願いします!
まずは、ソートの関数内にここまでのデバッグコードを入れて、実際に動かせるプログラムにしましょうか。
はい。やってみます!
main.c
#include <stdio.h>
#include <stdlib.h>

/* 整数の配列を小さい順にソートする。
 * - pNumbers: ソートする配列の先頭アドレス
 * - numberCount: ソートする要素数
 * pNumbersが0(NULL)の場合の動作は未定義。
 */
void SortNumbers(int *pNumbers, size_t numberCount) {

  /* 引数をチェック */
  if (pNumbers) {
    ; /* OK */
  } else {
    ; /* NG(pNumbersが0になっている) */
  }

  /* バブルソート */
  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;
      }
    }
  }

  /* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
  for (size_t i=1; i<numberCount; i++) {
    if (pNumbers[i - 1] <= pNumbers[i]) {
      ; /* OK */
    } else {
      ; /* NG(期待どおりにソートできていない) */
    }
  }

}

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;
}
できました!ソートする関数を実際に呼び出すプログラムにしてみたんですけど、どうですか?
うん。いい感じよ。ここまでに考えたことを、うまく組み込めたみたいね。
えへへ。
さてと。このプログラムには、バグが検出されるケースが2通りあるわね。
はい。「NG」ってコメントがある2つですね。
そう。開発中は、この2つのケースに引っかかったら気付くようにしたいわね。そのためには……
printfでエラーメッセージを表示するとか?
そうそう。そんな感じで、バグが目立つようにしたいのよ。あとは、プログラムを異常終了させちゃいましょう。
え、異常終了ですか?
うん。そしたら「ぎゃー!」ってなるから、バグを直さざるをえなくなるでしょ?
な、なるほど……わざと異常終了させるなんて、考えたことなかったかも。
ふふふ。C言語ならabort()、C++を使っているならterminate()を使えばいいわね。1つやってみてくれる?
えっと、エラーメッセージを表示して異常終了と……
  /* 引数をチェック */
  if (pNumbers) {
    ; /* OK */
  } else {
    printf("NG:pNumbersが0です!\n");
    abort();
  }
そうそう。じゃあ、せっかくだから、バグの検出を体験しておきましょうか。

ここまでできたら、実際にバグがあった場合に何が起こるのか体験しておくといいでしょう。やり方は、実際に「NG」になる呼び出し方をしてみるだけです。

今回の例では、第1引数を0(NULL)にしてSortNumbers()を呼び出してみればいいですね。実行すると、エラーメッセージが表示されて、プログラムが停止する様子を確認できます。

おおぅ……異常終了しましたよ。これ、急に出たらほんとに「ぎゃー!」って声が出るかも。
あはは、そうかもね。じゃ、次のテクニックいくわよ。
わ、次があるんですね!

これで、バグが検出されたことが目立つようになりました。それがどのようなバグなのかも、メッセージを読めば分かります。でも、このメッセージの内容は、直前にあるif文の条件式を見れば明らかですね。

そのため、ここで重要となる情報は「どのようなバグなのか」ではありません。代わりに、「どこで検出されたバグなのか」が表示されるように改良してみましょう。C言語では、__FILE____LINE__を使うのがおすすめです。

  • __FILE__:いま見ているソースコードのファイル名を表すマクロ
  • __LINE__:いま見ているソースコードの行番号を表すマクロ
バグが検出されたときのメッセージを、__FILE____LINE__の表示に直してみるわね。
例えば、こんな感じ。
  /* 引数をチェック */
  if (pNumbers) {
    ; /* OK */
  } else {
    printf("NG:%s (%d)\n", __FILE__, __LINE__);
    abort();
  }
へぇー、こんなマクロがあるんですね。
便利でしょ。これで、バグがどのファイルの何行目で検出されたのかが分かるようになるわ。
なるほどって感じです。メッセージを見たら、すぐにバグの調査にとりかかれるんですね!
そういうこと!じゃあ、2つ目のデバッグコードも、同じように直してみましょうか。
えっと、ソート結果をチェックしてるところですね……
  /* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
  for (size_t i=1; i<numberCount; i++) {
    if (pNumbers[i - 1] <= pNumbers[i]) {
      ; /* OK */
    } else {
      printf("NG:%s (%d)\n", __FILE__, __LINE__);
      abort();
    }
  }
やってみて気付いたんですけど、エラーメッセージを表示して異常終了するところは、まったく同じように書けるんですね。
うん。そこはバグを検出したあとの処理だから一緒なのよね。
そっか。違うのは「どうなったらバグか」っていう条件のところだけなんですね。
そういうことね。
ここがポイント!
デバッグコードで検出したバグは、とにかく目立つようにしましょう。プログラムを異常終了させる方法がおすすめです。

デバッグコードを開発中に限定する

さてと。これでバグを目立たせるテクニックは覚えたわね。
はい。
そしたら、もう1つやりたいことがあったと思うのだけど、覚えてるかしら?
あれですよね、リリース後はバグがあっても目立たないようにするっていう……
そうそう。ちゃんと覚えてたわね。

ソフトウェア開発には、矛盾する2つの願いがあります。

  • 開発中は問題があったら早く直したいので、バグは目立つようにしたい
  • リリース後は、もしバグが残っていたとしても目立たないようにしたい

これは、ソフトウェアには2つのバージョンが必要だということを意味しています。「開発中のバージョン」と、「リリース用のバージョン」です。といっても、同じような開発を2回行わなければならないという意味ではありません。ソースファイルをコンパイルするときに、どちらのバージョンの実行ファイルを生成するか選べるようにするということです。

IDEを使っている人なら、開発中は「デバッグビルド」、最終的には「リリースビルド」を行えばいいと知っているのではないでしょうか。これは、2つのバージョンを選択するための仕組みです。

ここで、マクロをもう1つ覚えましょう。

  • NDEBUG:リリース用のバージョンをコンパイルするときにだけ定義される

NDEBUGは、C言語で標準的に利用できるマクロです。頭に「N」が付いているので、「Not DEBUG」という意味ですね。これと、条件付きコンパイルを組み合わせると、開発中バージョンにだけデバッグコードを付加できます。

具体的には、次のようにします。

  #ifndef NDEBUG
  ここにデバッグコード
  #endif

ここで、#ifdefではなくて#ifndefを使っている点に注目しましょう。こちらも「n」が付いているので、「if not defined」という意味ですね。つまり、「もしNDEBUGが定義されていないなら」という条件により、開発中のバージョンのみでデバッグコードが有効になるのです。

C言語では「#ifndef NDEBUG」っていうお決まりのフレーズで、開発中にだけ有効なコードを埋め込めるということね。
なるほどー。なんか、いかにも「テクニック」っていう感じで、やってみたくなりますね。
いいわね。じゃ、やってみましょうか。
はーい!
えっと、1つ目のデバッグコードは……
  /* 引数をチェック */
  #ifndef NDEBUG
  if (pNumbers) {
    ; /* OK */
  } else {
    printf("NG:%s (%d)\n", __FILE__, __LINE__);
    abort();
  }
  #endif
そうそう。あってるわよ。
じゃあ、2つ目のデバッグコードも同じように……
  /* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
  #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#endifで囲むだけでいいの。
簡単ですね!
でしょ。こうしておけば、リリース後はデバッグコードが無効になるということね。

このようなデバッグコードは、リリース用のバージョンでは「なかったこと」になります。そのため……

  • たとえバグが残っていたとしても、目立ちにくくなる

という点のほかに、次のようなメリットも得られます。

  • デバッグコードが、リリース後の実行速度を遅くする心配はない
  • デバッグコードにより、リリース用の実行ファイルが大きくなる心配はない
なるほど。行数が増えた分だけプログラムも大きくなっちゃう気がしてたんですけど。
デバッグコードは、リリース用のバージョンでは条件付きコンパイルで無視されるから……
最終的には影響が出ないんですね!
そういうこと!ただ、ちょっとだけ気を付けたいところもあるのよ。

条件付きコンパイルでデバッグコードを書くときは、注意すべき点もあります。それは、リリース後も必要となる機能の一部を、デバッグコードに含めてはならないということです。

もしも必要な機能を含めてしまうと、その部分がリリース用のバージョンで無視され、機能の欠損を招いてしまいます。デバッグコードはあくまでバグを検出するためのものと考え、それ以外のコードは含めないようにしましょう。

デバッグコードがあってもなくても、プログラムは同じように機能しなければならないということね。
ふむふむ。デバッグコードが機能の一部になってしまうと問題なんですね。
そういうことね。まぁ現実的には、「バグを捕まえる」っていう本来の目的を忘れなければ、だいたい大丈夫だと思うわよ。
ここがポイント!
デバッグコードは、リリース後の動作に影響を与えないように書きましょう。そのためには、条件付きコンパイルを使うのが簡単です。

デバッグコードを書きやすくするには?

さてさて。デバッグコードの基本的な書き方は分かったかしら。
はい!ていうか、もう実戦でどんどん使ってみたいと思ってますけど……
あー、待って待って。あと1つ、ぜひ覚えておきたいテクニックがあるのよ。
そうなんですか?(あれれ?この流れ、前にもあったような……)
次は、デバッグコードをもっとスマートに書く方法、assertを伝授するわ。
やったー!