実行時エラーをチェックするには

ここまで、ちょっと難しい話が続いたかしらね。
そうですねー。デバッグコードの話とか、けっこう難しかったです。
じゃあ、今回はもう少し単純なプログラムからはじめましょうか。
はーい!

次は、C言語のプログラムの一部です。整数1つ分のメモリーを確保して数値を代入し、表示しています。

  int *p = malloc(sizeof(int));
  *p = 123;

  printf("%d\n", *p);
これを実行したら、どんな表示になるかしら?
これは……123って表示されると思います。
そうね。でも、このプログラムには少し問題があるの。メモリー不足になるとmalloc()は……
あ、それ分かるかも!
たしかmalloc()を使うときは、こんな感じでif文が必要なはず……
  int *p = malloc(sizeof(int));
  if (p) {
    *p = 123;

    printf("%d\n", *p);
  }
正解よ。えらい!
えへん!malloc()if文のセットは、よく出てきますよね。いつもこんな書き方になってるなーって思って。
うんうん。それじゃあ、どうしてif文が必要なのかは説明できるかしら?
えっと……malloc()でちゃんとメモリーを確保できたかチェックするため、ですかね。
そのとおり。メモリーが足りないときは0(NULL)を返すっていうのが、malloc()の仕様ね。
だから次の処理をする前に、0(NULL)じゃないことを確認しないといけないんですね。

メモリーは、限られたリソースです。ほかのプログラムがたくさん動いているときなどは、必要なだけ確保できないことも考えられます。

このような、プログラムを実行しているときの状態によって起こるかもしれないエラーを「実行時エラー(ランタイムエラー)」といいます。実行時エラーは、必ずチェックしなければなりません。malloc()も、チェックが必要な関数の一つだということです。

とはいえ、上記のプログラムが実際にメモリーの確保に失敗するケースはほとんどないでしょう。現代的なOSには、「仮想メモリー」という仕組みが搭載されているためです。物理的なメモリーにハードディスクなどを組み合わせてデータの記憶場所をやりくりすることで、自由に使える仮想的なメモリー空間を作り出しているのです。

昔のパソコンではね、malloc()が実行時エラーを起こすことはけっこうあったのよ。
へぇー。そうだったんですか。
だからね、if文を書き忘れるっていうのはシビアな問題だったの。
メモリーが少ないと動かないプログラムになっちゃうってことですか?
動かないっていうより……大事なときに限ってクラッシュするプログラムになるのよ。
うわぁ。……でも、今のパソコンだったら安心なんですよね。
昔に比べればね。でも、やっぱりエラーのチェックが必要なことには変わりがないのよ。

エラーの発生頻度が低いからといって、それがエラーチェックをしなくてもいい理由にはなりません。むしろ頻度が低いエラーほど、実際に起こったときには重大なトラブルを引き起こすとも考えられます。

メモリーの確保以外にも、ファイルへの入出力や通信などは、いつ実行時エラーが発生してもおかしくない操作です。何かエラーを返す可能性のある関数を呼び出したら、必ず結果をチェックするよう心がけましょう。

assertと実行時エラー

ところで最近、assertの使い方を覚えたでしょう?
はい。どんどん使ってます!
ふふふ。そしたらね、malloc()のチェックにもassertは使えると思う?
えっ?

結論から言うと、malloc()が起こすような実行時エラーのチェックに、assertを使うことはできません。assertはプログラムに含まれる欠陥、つまりバグを検出するためのものだからです。assertが必要なのは開発中だけなので、リリース用のバージョンでは無視されます。

これに対して、実行時エラーはリリース後にもチェックする必要があります。実行時エラーは「メモリーが足りなくなった」のような、プログラムを実行中の状況によって発生するエラーだからです。何かassertとは別の方法で、エラーの有無をチェックしなければなりません。

assertは、リリース後には「なかったこと」になるんだったわよね。
そっか。だから、この書き方はダメで……
  int *p = malloc(sizeof(int));
  assert(p);
  ……
やっぱりif文でチェックするのがいいんですね。
そういうこと。具体的にはこんな感じで……
  int *p = malloc(sizeof(int));
  if (p) {
    /* 通常の処理 */
    ……
  } else {
    /* エラー処理 */
    ……
  }
通常はこう、でもエラーが発生したときはこう……っていうように、処理を分岐させるのが基本ね。
ここがポイント!
assertは開発中にバグを検出するためのもの。リリース後にも必要な実行時エラーのチェックには、if文など別の方法を使いましょう。
んー、assertに比べると、if文は行数が多くなっちゃいますね……
そうねぇ。少しめんどうだけど、そこはしっかり書いたほうがいいんじゃないかしら。
そうなんですか?
ちゃんとしたプログラムにしたかったら、エラー処理は手を抜けないところなのよ。
そういうものですかー。
ふふふ。じゃあ、もしも手を抜いてしまったらどうなるか?っていうのを考えてみましょうか。

エラーチェックで手を抜くとどうなる?

次のプログラムは、「謎のテキスト」を処理して「結果のテキスト」を抜き出すというものです。実行すれば、おそらく問題なく動作するでしょう。でも、エラーチェックをしていないために、いつクラッシュしてもおかしくないところが2カ所もあります。

main.c
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>

/* 謎のテキストから結果のテキストを抜き出して返す。
 * - pSecretText: 謎のテキスト
 * 結果のテキストは、呼び出し側がfree()で解放すること。
 * 結果のテキストを抜き出せない場合は、0(NULL)を返す。
 * pSecretTextが0(NULL)の場合の動作は未定義。
 */
char *ProcessSecretText(const char *pSecretText) {
  assert(pSecretText);

  char *pText = 0;
  int textLength = 0;

  /* 謎のテキストを1文字ずつ処理 */
  for (const char *pLetter=pSecretText; *pLetter!='\0'; pLetter++) {

    /* 数字は読み飛ばす */
    if (isdigit(*pLetter)) {
      continue;
    }

    /* 結果のテキストに1文字追加 */
    pText = realloc(pText, textLength + 2);
    pText[textLength] = *pLetter;
    pText[textLength + 1] = '\0';
    textLength += 1;
  }

  return pText;
}

int main(void) {
  char *pText = ProcessSecretText("47H5e3ll2o9!18");
  puts(pText);

  free(pText);
  pText = 0;

  return EXIT_SUCCESS;
}
ProcessSecretText()関数に「謎のテキスト」を渡しているのが分かるかしら?
えっと、"47H5e3ll2o9!18"ですね。ちょっと実行してみていいですか?
もちろん!
(カタカタカタ……)
実行結果
Hello!
わぁ!Hello!って出ましたよ!
「謎のテキスト」から数字を取り除いたものが、「結果のテキスト」として取り出されたわけね。
おもしろいですね!
ふふふ。でも、直すべきところが2カ所もあるのよ。

1カ所目のエラーチェック

まずは、関数のコメントに注目してみましょう。「結果のテキストを抜き出せない場合は、0(NULL)を返す。」と書かれています。これこそ、実行時エラーですね。

ところが、呼び出し側ではこのエラーをチェックしていません。

ProcessSecretText()の実行結果をチェックしていない!
  char *pText = ProcessSecretText("47H5e3ll2o9!18");
  puts(pText);

  free(pText);
  pText = 0;

このままでは、puts()関数に0(NULL)を渡してしまう恐れがあります。ProcessSecretText()関数の戻り値を、次のようにif文でチェックする必要があるでしょう。

ProcessSecretText()の実行結果をif文でチェック!
  char *pText = ProcessSecretText("47H5e3ll2o9!18");
  if (pText) {
    puts(pText);

    free(pText);
    pText = 0;
  } else {
    /* 結果のテキストを抜き出せなかった! */
    puts("Error");
  }

2カ所目のエラーチェック

次は、ProcessSecretText()関数の中身に注目しましょう。「結果のテキストに1文字追加」とコメントがある部分です。

ここでは、realloc()関数が使われていますね。これは、サイズを指定してメモリーを確保し直す標準関数です。malloc()関数と同様に、失敗すると0(NULL)を返すのがこの関数の仕様です。

ところが、この戻り値がチェックされていません。

realloc()の実行結果をチェックしていない!
    /* 結果のテキストに1文字追加 */
    pText = realloc(pText, textLength + 2);
    pText[textLength] = *pLetter;
    pText[textLength + 1] = '\0';
    textLength += 1;

このままでは、メモリーを確保できなかったのに文字を書き込んでしまう恐れがあります。ここでもif文を使って、realloc()関数の戻り値をチェックする必要があるでしょう。

realloc()の実行結果をif文でチェック!
    /* 結果のテキストに1文字追加 */
    {
      char *p = realloc(pText, textLength + 2);
      if (p) {
        pText = p;
      } else {
        /* メモリーを確保できなかった! */
        free(pText);
        pText = 0;

        break;
      }
    }
    pText[textLength] = *pLetter;
    pText[textLength + 1] = '\0';
    textLength += 1;

エラーチェックは「今やる」が基本

さて、2カ所にエラーチェックを追加すると、プログラムは次のようになります。これで「良いプログラムになった」といえるでしょうか?

main.c
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>

/* 謎のテキストから結果のテキストを抜き出して返す。
 * - pSecretText: 謎のテキスト
 * 結果のテキストは、呼び出し側がfree()で解放すること。
 * 結果のテキストを抜き出せない場合は、0(NULL)を返す。
 * pSecretTextが0(NULL)の場合の動作は未定義。
 */
char *ProcessSecretText(const char *pSecretText) {
  assert(pSecretText);

  char *pText = 0;
  int textLength = 0;

  /* 謎のテキストを1文字ずつ処理 */
  for (const char *pLetter=pSecretText; *pLetter!='\0'; pLetter++) {

    /* 数字は読み飛ばす */
    if (isdigit(*pLetter)) {
      continue;
    }

    /* 結果のテキストに1文字追加 */
    {
      char *p = realloc(pText, textLength + 2);
      if (p) {
        pText = p;
      } else {
        /* メモリーを確保できなかった! */
        free(pText);
        pText = 0;

        break;
      }
    }
    pText[textLength] = *pLetter;
    pText[textLength + 1] = '\0';
    textLength += 1;
  }

  return pText;
}

int main(void) {
  char *pText = ProcessSecretText("47H5e3ll2o9!18");
  if (pText) {
    puts(pText);

    free(pText);
    pText = 0;
  } else {
    /* 結果のテキストを抜き出せなかった! */
    puts("Error");
  }

  return EXIT_SUCCESS;
}
直す前のプログラムよりも、ちょっと行数が増えちゃいましたね。
そうね。エラーチェックは必要なものだから、それでいいのよ。
でも……なんだかデバッグした気がしないというか……これって、本当に直ったんですか?
じゃあ、またプログラムを実行してみてくれる?
はい。(カタカタカタ……)
実行結果
Hello!
あ、やっぱり!ぜんぜん最初と変わってないですよ!
そのとおり。直したのはエラーが発生した場合の動作だけだから、正常時の動作は何も変わっていないように見えるはずよね。
んんっ?そっか……そりゃそうですね。
きちんと直ったかどうか確かめるには、実際にエラーを発生させてみないとね。
えー。それって、すごく大変なのでは……
うん、すごく大変。エラーチェックで手を抜いてしまったプログラムを、あとから直すのは手間がかかるのよ。
はぁ。
かといって、直さずに放置しておくのも爆弾を抱えているようなものよね。
いつクラッシュしてもおかしくない状況ってことですもんね……
だからエラーチェックは最初から手を抜かない!それが大切よ。

実行時エラーのチェックを、あとからプログラムに追加するのは手間のかかる作業です。どうすれば「直った」といえるのか、よく考えなければならないためです。例えばメモリー不足のような、あまり発生しない状況をどうにかして作り出さなければなりません。

しかし、直すのが大変だからといって放置しておくと、いつか本当にエラーが発生してトラブルになってしまうかもしれません。リスクを避ける最善の方法は、常にエラーチェックを怠らないことだといえるでしょう。

エラーチェックは「今やる」が基本です。「ここはチェックが必要だぞ」と気付いたら、すぐにif文を書きましょう。これは、いつか起こるトラブルを減らすためにも、大切な心がけです。

ここがポイント!
実行時エラーをチェックしないプログラムは、爆弾を抱えているようなもの。エラーチェックで手を抜かないように、いつも心がけておきましょう!