Bazen bilgisayarımda "saf sanal işlev çağrısı" hatasıyla çöken programlar görüyorum.
Soyut bir sınıftan bir nesne yaratılamadığında bu programlar nasıl derlenir?
Bazen bilgisayarımda "saf sanal işlev çağrısı" hatasıyla çöken programlar görüyorum.
Soyut bir sınıftan bir nesne yaratılamadığında bu programlar nasıl derlenir?
Yanıtlar:
Bir kurucu veya yıkıcıdan sanal bir işlev çağrısı yapmaya çalışırsanız, sonuç olabilir. Bir kurucu ya da yıkıcıdan sanal bir işlev çağrısı yapamayacağınız için (türetilmiş sınıf nesnesi inşa edilmemiş ya da zaten yok edilmiş), temel sınıf sürümünü çağırır ve bu, saf bir sanal işlev durumunda, yok.
(Canlı demoyu buradan izleyin )
class Base
{
public:
Base() { doIt(); } // DON'T DO THIS
virtual void doIt() = 0;
};
void Base::doIt()
{
std::cout<<"Is it fine to call pure virtual function from constructor?";
}
class Derived : public Base
{
void doIt() {}
};
int main(void)
{
Derived d; // This will cause "pure virtual function call" error
}
doIt()
Yapıcıdaki çağrı kolayca sanallaştırılır ve Base::doIt()
statik olarak gönderilir , bu da bir bağlayıcı hatasına neden olur. Gerçekten ihtiyacımız olan şey, dinamik bir gönderim sırasında dinamik tipin soyut temel tip olduğu bir durumdur .
Base::Base
sanal olmayan çağırın . f()
doIt
Saf sanal işlevlere sahip bir nesnenin kurucusundan veya yıkıcısından sanal bir işlevi çağırmanın standart durumunun yanı sıra, nesne yok edildikten sonra sanal bir işlevi çağırırsanız, saf bir sanal işlev çağrısı (en azından MSVC'de) alabilirsiniz. . Açıkçası, bu denemek ve yapmak oldukça kötü bir şey ama arayüz olarak soyut sınıflarla çalışıyorsanız ve karıştırırsanız, o zaman görebileceğiniz bir şeydir. Başvurulan sayılan arayüzleri kullanıyorsanız ve bir ref sayım hatası varsa veya çok iş parçacıklı bir programda bir nesne kullanımı / nesne yok etme yarış koşulunuz varsa, muhtemelen daha olasıdır ... Bu tür saf çağrılarla ilgili olan şey şudur: ctor ve dtor'daki sanal aramaların 'olağan şüphelileri' için bir kontrol netleşeceğinden, neler olup bittiğini anlamak genellikle daha az kolaydır.
Bu tür sorunlarda hata ayıklamaya yardımcı olmak için, MSVC'nin çeşitli sürümlerinde, çalışma zamanı kitaplığının purecall işleyicisini değiştirebilirsiniz. Bu imzayla kendi işlevinizi sağlayarak bunu yaparsınız:
int __cdecl _purecall(void)
ve çalışma zamanı kitaplığını bağlamadan önce bağlama. Bu, SİZİN bir saf arama algılandığında olacakları kontrol etmenizi sağlar. Kontrole sahip olduğunuzda, standart işleyiciden daha yararlı bir şey yapabilirsiniz. Saf çağrının gerçekleştiği yerin yığın izini sağlayabilen bir işleyicim var; daha fazla ayrıntı için buraya bakın: http://www.lenholgate.com/blog/2006/01/purecall.html .
(İşleyicinizi MSVC'nin bazı sürümlerinde kurmak için _set_purecall_handler () 'ı da çağırabileceğinizi unutmayın).
_purecall()
normalde edecek Silinen örneğinin bir yöntemi çağırmak üzerinde meydana çağırma değil taban sınıfı ile ilan edilmiştir durumunda ne __declspec(novtable)
optimizasyonu (Microsoft özgü). Bununla, nesne silindikten sonra geçersiz kılınmış bir sanal yöntemi çağırmak tamamen mümkündür, bu da sizi başka bir biçimde ısırana kadar sorunu maskeleyebilir. _purecall()
Tuzak senin arkadaşın!
Genellikle sarkan bir işaretçi aracılığıyla bir sanal işlevi çağırdığınızda - büyük olasılıkla örnek zaten yok edilmiştir.
Daha "yaratıcı" nedenler de olabilir: belki de nesnenizin sanal işlevin uygulandığı kısmını dilimlemeyi başardınız. Ancak genellikle yalnızca örnek zaten yok edilmiş durumdadır.
Saf sanal işlevlerin yok edilen nesneler nedeniyle çağrıldığı senaryosuna rastladım, Len Holgate
zaten çok güzel bir cevabı var , bir örnekle biraz renk eklemek istiyorum:
Türetilmiş sınıf yıkıcı, vptr noktalarını, saf sanal işlevi olan Base sınıfı vtable'a sıfırlar, bu nedenle sanal işlevi çağırdığımızda, aslında saf virütal olanları çağırır.
Bu, bariz bir kod hatası veya çok iş parçacıklı ortamlarda karmaşık bir yarış durumu senaryosu nedeniyle olabilir.
İşte basit bir örnek (optimizasyon kapalıyken g ++ derlemesi - basit bir program kolayca optimize edilebilir):
#include <iostream>
using namespace std;
char pool[256];
struct Base
{
virtual void foo() = 0;
virtual ~Base(){};
};
struct Derived: public Base
{
virtual void foo() override { cout <<"Derived::foo()" << endl;}
};
int main()
{
auto* pd = new (pool) Derived();
Base* pb = pd;
pd->~Derived();
pb->foo();
}
Ve yığın izleme şöyle görünür:
#0 0x00007ffff7499428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1 0x00007ffff749b02a in __GI_abort () at abort.c:89
#2 0x00007ffff7ad78f7 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3 0x00007ffff7adda46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4 0x00007ffff7adda81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007ffff7ade84f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6 0x0000000000400f82 in main () at purev.C:22
Vurgulamak:
nesne tamamen silinirse, yani yıkıcı çağrılırsa ve memroy geri alınırsa, Segmentation fault
bellek işletim sistemine geri döndüğünde ve program ona erişemediğinde bir tane alabiliriz. Dolayısıyla, bu "saf sanal işlev çağrısı" senaryosu genellikle nesne bellek havuzuna tahsis edildiğinde gerçekleşir, bir nesne silinirken, temeldeki bellek işletim sistemi tarafından geri alınmaz, hala orada işlem tarafından erişilebilir durumdadır.
Soyut sınıf için bazı iç nedenlerden dolayı (bir tür çalışma zamanı türü bilgisi için gerekli olabilir) oluşturulmuş bir vtbl olduğunu ve bir şeyler ters gittiğini ve gerçek bir nesne aldığını tahmin ediyorum. Bu bir böcek. Tek başına bu, olmayacak bir şeyin olduğunu söylemelidir.
Saf spekülasyon
düzenleme: söz konusu durumda yanılmışım gibi görünüyor. OTOH IIRC bazı diller, kurucu yıkıcıdan vtbl çağrılarına izin verir.
VS2010 kullanıyorum ve yıkıcıyı doğrudan genel yöntemden çağırmayı denediğimde, çalışma zamanı sırasında "saf sanal işlev çağrısı" hatası alıyorum.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void SomeMethod1() { this->~Foo(); }; /* ERROR */
};
Bu yüzden ~ Foo () 'nun içindekini özel yöntemi ayırmak için taşıdım, sonra bir cazibe gibi çalıştı.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void _MethodThatDestructs() {};
void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
Borland / CodeGear / Embarcadero / Idera C ++ Builder kullanıyorsanız,
extern "C" void _RTLENTRY _pure_error_()
{
//_ErrorExit("Pure virtual function called");
throw Exception("Pure virtual function called");
}
Hata ayıklama sırasında, koda bir kesme noktası yerleştirin ve IDE'deki çağrı yığınını görün, aksi takdirde bunun için uygun araçlara sahipseniz, istisna işleyicinizde (veya bu işlevde) çağrı yığınını günlüğe kaydedin. Bunun için kişisel olarak MadExcept kullanıyorum.
PS. Orijinal işlev çağrısı [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp içindedir.
İşte bunun gerçekleşmesinin sinsi bir yolu. Bu aslında bugün başıma geldi.
class A
{
A *pThis;
public:
A()
: pThis(this)
{
}
void callFoo()
{
pThis->foo(); // call through the pThis ptr which was initialized in the constructor
}
virtual void foo() = 0;
};
class B : public A
{
public:
virtual void foo()
{
}
};
B b();
b.callFoo();
I had this essentially happen to me today
açıkça doğru değil, çünkü basitçe yanlış: saf bir sanal işlev yalnızca callFoo()
bir kurucu (veya yıkıcı) içinde çağrıldığında çağrılır, çünkü şu anda nesne hala (veya zaten) A aşamasındadır. İşte kodunuzun sözdizimi hatası olmadan çalışan bir sürümüB b();
- parantezler onu bir işlev bildirimi yapıyor, bir nesne istiyorsunuz.