Kırılan "pratik" ("buggy" hecelemenin komik yolu) kodlarından bazıları şöyle görünüyordu:
void foo(X* p) {
p->bar()->baz();
}
ve p->bar()
bazen boş bir işaretçi döndürdüğü gerçeğini hesaba katmayı unutur, bu da çağırmak baz()
için kayıttan çıkarmanın tanımsız olduğu anlamına gelir .
Bozulan tüm kodlar açık if (this == nullptr)
veya if (!p) return;
kontrol içermiyordu. Bazı durumlar, herhangi bir üye değişkenine erişim sağlamayan işlevlerdi ve bu nedenle düzgün çalışıyor gibi görünüyordu . Örneğin:
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);
}
Bu kodda func<DummyImpl*>(DummyImpl*)
bir boş gösterici ile arama yaptığınızda , gösterilecek göstericinin "kavramsal" bir referansı vardır p->DummyImpl::valid()
, ancak aslında üye işlevi false
erişmeden geri döner *this
. Bu return false
satır içi işaretlenebilir ve bu nedenle pratikte işaretçiye hiç erişilmesine gerek yoktur. Yani bazı derleyiciler ile Tamam gibi görünüyor: nere dereferencing için segfault yok p->valid()
, yanlış, bu yüzden boş çağırıcılar için do_something_else(p)
denetler ve hiçbir şey yapmaz , kod çağrıları . Çökme veya beklenmedik davranış gözlenmez.
GCC 6 ile hala çağrıyı alırsınız p->valid()
, ancak derleyici şimdi bu ifadeden p
boş olmamalıdır (aksi takdirde p->valid()
tanımsız davranış olacaktır) ve bu bilgileri not eder. Çıkarılan bilgi optimize edici tarafından kullanılır, böylece çağrıyı do_something_else(p)
satır içine alırsa, if (p)
kontrol artık gereksiz kabul edilir, çünkü derleyici boş olmadığını hatırlar ve böylece kodu satır içine alır :
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);
}
}
Bu gerçekten boş bir işaretçi dereference ve bu nedenle daha önce çalışmak için görünen kod çalışmayı durdurur.
Bu örnekte, func
önce null olup olmadığını denetleyen hata (veya arayanlar bunu asla null ile çağırmamış olmalıdır):
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}
Unutulmaması gereken önemli bir nokta, bunun gibi çoğu optimizasyonun derleyicinin "ah, programcı bu işaretçiyi null'a karşı test ettiğini, sadece sinir bozucu olarak kaldıracağımı" söylemesi değil. Olan şey şu ki, satır içi ve değer aralığı yayılımı gibi çeşitli fabrika işletmesi optimizasyonları, bu kontrolleri gereksiz kılmak için bir araya geliyor, çünkü daha önceki bir kontrol veya bir dereference'den sonra geliyorlar. Derleyici, bir işlevde A noktasında bir işaretçinin boş olmadığını biliyorsa ve aynı işlevde daha sonraki bir B noktasından önce işaretçi değiştirilmezse, B'de de boş olmadığını bilir. A ve B noktaları aslında başlangıçta ayrı işlevlerde olan kod parçaları olabilir, ancak şimdi tek bir kod parçası halinde birleştirilmiştir ve derleyici, işaretçinin daha fazla yerde boş olmadığı bilgisini uygulayabilir.