例えば、以下の関数があったとします。
const char* nameOfUser( UserData *user )
{
return user->name();
}
もし user が NULL だった場合、この関数は落ちます。
user が NULL になる可能性が理論上あり得るあるなら NULL チェックをすれば良いですが
user が NULL になる可能性が理論上あり得ない場合、NULL チェックは不要と判断するでしょう。
しかし、「理論上あり得ない」と「実際にあり得ない」はイコールではありません。
複雑なプログラムになればなるほど、我々の小さな脳で導き出した理論は間違う可能性が高くなるのです。
そこで、assert を使うことにします。
const char* nameOfUser( UserData *user )
{
assert( user != NULL );
return user->name();
}
このコードだったら、もし user が NULL の場合、アサーションが発生してくれます。
しかしこれは Debug ビルドの場合のみです。
一般的に(特にアプリ開発では)、Release ビルドの場合は NDEBUG が定義されているため、アサーションが無効化されます。
つまり、Release ビルドでは、上記のコードは最初のコードと同一になり、user->name() にアクセスした際に落ちます。
「絶妙のタイミングで関数が呼ばれた時」とか「1万回に1回の確率」などで user が NULL になるとしたらどうでしょう?
スレッドの排他処理が中途半端だと、そういうことも有り得ます。
もちろんバグなのですが、再現性が低ければ、それがデバッグ中やテスト中に発覚しない可能性もあります。
つまり、user が NULL になる可能性がある状態のままリリースされてしまうのです。
ユーザーは様々な使い方をするので、この再現性が低い状況を起こしてしまうでしょう。
そうなるとユーザーの環境でクラッシュが発生してしまいます。
これは避けなければなりません。
そこで以下のように実装します。
const char* nameOfUser( UserData *user )
{
if ( user == NULL ) {
assert( false );
return NULL;
}
return user->name();
}
assert の引数を false にして、「Debug ビルドだと必ず落ちる」というパスにしてしまいましょう。
こうすると、Release ビルドでアサーションが無効になっても、クラッシュすることなく関数が終了します。
(もちろん関数の戻りが NULL であることを考慮した設計にする必要はあります。)
こうして世界からまた1つクラッシュが減るのです。
これは本来の assert の使い方ではないかもしれません。
if ( user == NULL ) {
assert( false );
return NULL;
}
よりも
assert( user != NULL );
の方が意図が分かりやすいのも確かです。
しかし、この書き方にもメリットが多いと思います。
そもそも、今回の例で言うと関数の頭で NULL チェックすべきです。
たとえ、理論上 NULL になることが有り得ない場合でも。
例えば、関数を呼ぶ側でチェックしてる場合、不要と感じるのは最もですが、その意図を知らない第3者が別のところで関数を呼んだ場合、そこで NULL チェックをする保証はありません。
NULL チェックはするに越したことはないのです。
NULL チェックを省略する理由としては、「冗長だから」とか「チェックの分、処理が遅くなるなら」ということでしょうが、近年のアプリ環境からすれば誤差みたいなものでしょう。
NULLチェックに限らず、関数内での前提条件は全てチェックすべきだと思っています。
もちろん限度はありますが。
今までの話の流れだと、assert の引数を false 固定にする意図が
「Release ビルドで assert してもクラッシュしないようにしよう」
ということになってしまいますが、実はこれは結果であり、本来の意図とは異なります。
本来の意図は
「冗長でもチェックはしよう。もし理論上有り得ないなら assert しちゃえ」
です。
assert(false)
を、「一応チェックしているけど、俺は理論上有り得ないと思ってるからね!」という意思表明として使いましょう。