この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。
33. null ポインターは逆参照しない
この問題は、GIT のソースコードで見つかりました。このコードにはエラーが含まれています。PVS-Studio アナライザーは、次の診断を出力します。
V595 The ‘tree’ pointer was utilized before it was verified against nullptr. Check lines: 134, 136. (V595 ‘tree’ ポインターは nullptr に対して検証される前に使用されています。次の行を確認してください: 134、136。)
void mark_tree_uninteresting(struct tree *tree) { struct object *obj = &tree->object; if (!tree) return; .... }
説明
null ポインターの逆参照は、未定義の動作を引き起こすため、良くないことは明確です。その背後にある理論的な根拠については、皆さんも同意されるでしょう。
しかし、その実践については、さまざまな意見があります。特定のコードが正しく動作することを主張するプログラマーは常にいます。彼らのコードは、これまで問題なく動作してきたからです。このセクションは、そういうプログラマーの考え方を変えるためのものです。
私はあえて、さまざまな意見が出るであろう例を選択しました。tree ポインターが逆参照された後、クラスメンバーはそれを使用するだけでなく、このメンバーのアドレスを評価しています。(tree == nullptr) の場合、メンバーのアドレスは全く使用されず、関数は終了します。多くのプログラマーは、これは正しいコードであると考えます。
しかし、そうではありません。このようなコードは記述すべきではありません。例えば、null アドレスへ値を書き込んだ場合、未定義の動作が必ずプログラムのクラッシュになるとは限りません。未定義の動作は、何にでもなりえます。null に等しいポインターを逆参照するとすぐに未定義の動作になります。プログラムの動作について、ここでこれ以上議論することは意味がありません。どのような動作にもなりえるのですから。
未定義の動作の兆候の 1 つは、コンパイラーが “if (!tree) return;” ブロック全体を削除する可能性があることです。コンパイラーは、ポインターはすでに逆参照されているため、null ではないと見なし、チェックを削除できると判断します。これは、プログラムのクラッシュを引き起こす多数のシナリオの 1 つです。
詳しくは、「null ポインターの逆参照が引き起こす未定義の動作」をご覧になることを推奨します。
正しいコード
void mark_tree_uninteresting(struct tree *tree) { if (!tree) return; struct object *obj = &tree->object; .... }
推奨事項
すべて問題なく動作しているように見える場合であっても、未定義の動作に注意すべきです。未定義の動作はリスクが大きすぎます。前述のように、未定義の動作は、どのような形で表れるか想像することが困難です。すべてが問題なく動作するように見える場合であっても、未定義の動作を回避するようにします。
未定義の動作が何か分かっており、他のプログラマーにはできないことを自分はできると考えるプログラマーがいるかしれません。そして、すべてが問題なく動作したとしても、それは間違っています。次のセクションでは、未定義の動作の危険性について述べます。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。