壊れた「実用的な」(「バギー」を綴る面白い方法)コードの一部は、次のようになります。
void foo(X* p) {
p->bar()->baz();
}
またp->bar()
、ヌルポインターを返すことがあるという事実を説明するのを忘れていました。つまり、それを呼び出すために逆参照することbaz()
は定義されていません。
壊れていたではないすべてのコードは、明示的に含まれているif (this == nullptr)
かif (!p) return;
をチェックします。一部のケースは、メンバー変数にアクセスしない単純な関数であり、問題なく動作するように見えました。例えば:
struct DummyImpl {
bool valid() const { return false; }
int m_data;
};
struct RealImpl {
bool valid() const { return m_valid; }
bool m_valid;
int m_data;
};
template<typename T>
void do_something_else(T* p) {
if (p) {
use(p->m_data);
}
}
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else
do_something_else(p);
}
このコードfunc<DummyImpl*>(DummyImpl*)
では、nullポインターを使用して呼び出すとp->DummyImpl::valid()
、callへのポインターの「概念的な」逆参照がありますが、実際には、メンバー関数はfalse
にアクセスせずに戻るだけ*this
です。これreturn false
はインライン化できるため、実際にはポインタにアクセスする必要はまったくありません。したがって、一部のコンパイラでは問題なく動作するように見えます。nullを逆参照するためのsegfaultはなく、p->valid()
falseであるため、コードはdo_something_else(p)
nullポインタをチェックするを呼び出し、何もしません。クラッシュや予期しない動作は見られません。
GCC 6を使用しても引き続きが呼び出されますp->valid()
が、コンパイラーはp
null ではない式(そうでない場合p->valid()
は未定義の動作)からその式を推測し、その情報を書き留めます。推定された情報はオプティマイザによって使用されるため、への呼び出しdo_something_else(p)
がインライン化された場合if (p)
、コンパイラはそれがnullではないことを記憶し、コードをインライン化するため、チェックは冗長と見なされます。
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else {
// inlined body of do_something_else(p) with value propagation
// optimization performed to remove null check.
use(p->m_data);
}
}
これは実際にはnullポインターを逆参照するため、以前は機能していたコードが機能しなくなります。
この例では、バグはにありfunc
、最初にnullをチェックする必要がありました(または、呼び出し元がnullで呼び出してはなりませんでした)。
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}
覚えておくべき重要な点は、このようなほとんどの最適化は、コンパイラーが「ああ、プログラマーがこのポインターをnullに対してテストしたので、煩わしいために削除する」というものではないということです。何が起こるかというと、インライン化や値の範囲の伝播など、さまざまな一般的な最適化が組み合わされて、これらのチェックが冗長になるためです。これは、以前のチェックまたは逆参照の後に行われるためです。コンパイラーが、ポインターが関数内の点Aで非ヌルであることを認識し、ポインターが同じ関数内の後続の点Bの前に変更されない場合、コンパイラーは、ポインターもBで非ヌルであることを認識します。ポイントAとBは、実際には元々別々の関数に含まれていたコードの一部である可能性がありますが、現在は1つのコードに結合されており、コンパイラーは、ポインターがより多くの場所で非ヌルであるという知識を適用できます。