「実行時エラー(ランタイムエラー)」とは、プログラムを実行しているときの状態によって起こるかもしれないエラーのことでした。例えばメモリー不足やファイル入出力の失敗、通信エラーなどが考えられます。
ここでは、「設定をファイルから読み込む」という動作について考えてみましょう。設定項目は、次のような構造体にまとめられているとします。
/* 設定項目をまとめた構造体 */
typedef struct {
……
} Configuration;
これを読み込む関数は、例えば次のような形になるでしょう。読み込みに成功したかどうかを、bool
型の戻り値で返します。正常に読み込めた場合は、引数のポインタ経由で設定値を受け取れます。
bool LoadConfiguration(Configuration *pConfiguration) {
……
}
bool
型にして……
この関数の呼び出し側は、次のようになるでしょう。エラーがないかどうかをif
文でチェックして、処理を振り分けています。
Configuration conf;
if (LoadConfiguration(&conf)) {
/* 通常の処理 */
……
} else {
/* エラー処理:設定を読み込めなかった! */
……
}
しかし、エラーチェックが必要なことを忘れて、うっかり次のように書いてしまうかもしれません。
Configuration conf;
LoadConfiguration(&conf);
/* 通常の処理(エラーチェックを忘れている!) */
;
こういったミスは、いくら注意していても完全になくせるわけではありません。そこで、どうすればミスに気付けるかを考えることが大切になってきます。例えば、コードレビューを実施するのは、ミスの早期発見に効果的な方法の一つです。
失敗しない関数をつくる
上記の「設定をファイルから読み込む関数」は、どのような場合に実行時エラーを起こすでしょうか。例えば、以下のようなケースが考えられます。
- ファイルが存在しなかった
- ファイルは存在するが、読み込めなかった
- ファイルを読み込めたが、そのデータが破損していた
関数内でこうした問題が発生したとき、ほんとうにエラーを返す必要があるのかどうか考えてみましょう。もし、関数が自力でエラーに対処できるのであれば、それで十分かもしれません。よく使う関数ほど、エラーを返さない仕様を検討してみる価値は高いといえるでしょう。
エラーを返さない関数は、例えば次のような形になります。読み込んだ設定値を戻り値として返すだけの、ごく単純なつくりです。
Configuration LoadConfiguration(void) {
……
}
エラーチェックが不要なため、呼び出し側もシンプルになります。
Configuration conf = LoadConfiguration();
/* 通常の処理 */
;
ただし、ほんとうにエラーが発生しなくなったわけではなく、発生しても関数内で対処するよう工夫されているのです。つまり、この関数はあたかもエラーなどないかのように振る舞うため、呼び出し側でエラーチェックについて気にする必要がなくなったということです。
関数内でエラーに対処する方法としては、例えば以下が考えられます。
- 設定ファイルを復旧して読み込み直す
- ファイルからの読み込みをあきらめて、デフォルトの設定値を返す
- エラーログを出力して、プログラムを緊急終了する(呼び出し元には戻らない)
もちろん、実際にどのような工夫でエラーに対処するのがベストなのかは、何を作っているのかによって変わってきます。毎日バックアップをとっているなら設定ファイルを復旧するのが簡単かもしれないし、緊急終了はどうしても避けたいという場合もあるでしょう。
呼び出し側のバグとみなす
次のように、表情の種類が定義されているとします。「幸せな顔」と「悲しい顔」です。
typedef enum {
Happy,
Sad
} FaceType;
この値を受け取って、顔文字を表示する関数を考えてみましょう。
bool PrintFace(FaceType type) {
bool bSuccess = true;
switch (type) {
case Happy:
puts("(^o^)");
break;
case Sad:
puts("(T_T)");
break;
default:
bSuccess = false;
}
return bSuccess;
}
Happy
かSad
かによって、対応する顔文字を表示するんですね。
default
のところが実行されるから、false
が返されます!
この関数は、正常に処理できたかどうかを戻り値として返します。次のような方針で作られているためです。
- 引数には、ひとまずどのような値でも受け入れる
- 処理できない値が引数に与えられたときは、エラーを返す
つまり、引数の値によっては実行時エラーを返すため、呼び出し側でのエラーチェックが必要な関数になっています。でも、引数は列挙型(enum
)ですね。そもそも、どのような値でも受け入れる必要があるでしょうか?
実際のところ、列挙型に定義されていない値をこの関数に渡したいケースは考えられません。もしもそのような値を渡しているとしたら、どこかで論理(ロジック)が破綻しているのだといえます。
そこで、次のような方針で関数のつくりを考え直してみることにしましょう。
- 引数には、列挙型で定義された値しか受け付けない
これは、関数の定義域を明確にするということです。これにより、エラーを返さない関数として書き換えることができます。具体的には、次のようになるでしょう。
void PrintFace(FaceType type) {
switch (type) {
case Happy:
puts("(^o^)");
break;
case Sad:
puts("(T_T)");
break;
default:
assert(false); /* ここに到達したらバグ! */
}
}
Happy
でもSad
でもない呼び出し方が、禁止になったのが分かるかしら?
default
のところに書かれているでしょう?
assert(false)
って、どういう意味ですか?
false
になっているから、絶対に成立しないassertね。つまり、この行が実行されたらバグだっていう意味なのよ。
ここでの処理がdefault
に到達するのは、引数がHappy
でもSad
でもないとき、つまり呼び出し側にバグがあった場合です。このような場所には、assert(false)
と書いておきましょう。そうすれば、本来は到達してはならない行が実行されてしまったことを検出できます。
switch
文のdefault
でよく使うテクニックなのよ。覚えておくといいわねー。
if
文の話をしたときだったかしら?