"Saf sanal işlev çağrısı" çökmeleri nereden geliyor?


Yanıtlar:


107

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
}

3
Derleyicinin genel olarak bunu yakalayamamasının bir nedeni var mı?
Thomas

21
Genel durumda, ctor'dan gelen akış herhangi bir yere gidebildiği ve herhangi bir yere gidebildiği için onu yakalayamaz ve saf sanal işlevi çağırabilir. Bu, Halting problemi 101.
shoosh

9
Cevap biraz yanlış: saf bir sanal işlev hala tanımlanmış olabilir, ayrıntılar için Wikipedia'ya bakın. Doğru ifade: mevcut olmayabilir
MSalters

5
Bu örneğin çok basit olduğunu düşünüyorum: 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 .
Kerrek SB

2
Fazladan bir yönlendirme seviyesi eklerseniz, bu MSVC ile tetiklenebilir: sırayla (saf) sanal yöntemi çağıran bir Base::Basesanal olmayan çağırın . f()doIt
Frerich Raabe

64

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).


1
Silinen bir örnekte bir _purecall () çağrısı alma hakkındaki işaretçi için teşekkürler; Bunun farkında değildim, ancak küçük bir test koduyla kendime kanıtladım. WinDbg'de bir ölüm sonrası çöplüğüne baktığımda, başka bir iş parçacığının, tamamen inşa edilmeden önce türetilmiş bir nesneyi kullanmaya çalıştığı bir yarışla uğraştığımı düşündüm, ancak bu konuya yeni bir ışık tutuyor ve kanıta daha iyi uyuyor gibi görünüyor.
Dave Ruske

1
Ben ekleyeceğiz Bir başka şey: _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!
Dave Ruske

Bu Dave'i tanımakta fayda var, son zamanlarda, olmam gerektiğini düşündüğüm halde saf arama almadığım birkaç durum gördüm. Belki de bu optimizasyondan dolayı hata yapıyordum.
Len Holgate

1
@LenHolgate: Son derece değerli cevap. Bu TAM OLARAK bizim sorun vakamızdı (yarış koşullarından kaynaklanan yanlış referans sayısı). Bizi doğru yöne yönlendirdiğiniz için çok teşekkür ederiz (bunun yerine v-tablosunun bozulmasından şüpheleniyorduk ve suçlu kodunu bulmaya çalışırken deliriyorduk)
BlueStrat

7

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.


4

Saf sanal işlevlerin yok edilen nesneler nedeniyle çağrıldığı senaryosuna rastladım, Len Holgatezaten çok güzel bir cevabı var , bir örnekle biraz renk eklemek istiyorum:

  1. Türetilmiş bir nesne oluşturulur ve işaretçi (Base sınıfı olarak) bir yere kaydedilir.
  2. Türetilmiş nesne silinir, ancak bir şekilde işaretçiye hala başvurulur
  3. Silinen Türetilmiş nesneyi gösteren işaretçi çağrılır

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 faultbellek 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.


0

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.


Derleyicide bir hata değil, eğer kastettiğiniz buysa.
Thomas

Şüpheniz doğru - C # ve Java buna izin veriyor. Bu dillerde, yapım aşamasında olan projelerin son türleri vardır. C ++ 'da, nesneler yapım sırasında tür değiştirir ve bu yüzden ve ne zaman soyut tipte nesnelere sahip olabilirsiniz.
MSalters

TÜM soyut sınıflar ve bunlardan türetilen gerçek nesneler, üzerinde hangi sanal işlevlerin çağrılması gerektiğini listeleyen bir vtbl'ye (sanal işlev tablosu) ihtiyaç duyar. C ++ 'da bir nesne, sanal işlev tablosu da dahil olmak üzere kendi üyelerini oluşturmaktan sorumludur. Oluşturucular temel sınıftan türetilmek için çağrılır ve yıkıcılar türetilmişten temel sınıfa çağrılır, bu nedenle soyut bir temel sınıfta sanal işlev tablosu henüz mevcut değildir.
fuzzyTew

0

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 */
};

0

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.


-2

İş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();

1
En azından benim vc2008'imde yeniden üretilemez, vptr, A'nın yüklenicisinde ilk kez başlatıldığında A'nın vtable'ına işaret ediyor, ancak daha sonra B tamamen başlatıldığında, vptr, B'nin vtable'ına işaret edecek şekilde değiştiriliyor, bu tamam
Baiyan Huang

coudnt ya vs2010 / 12 ile yeniden üret
makc

I had this essentially happen to me todayaçı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.
Wolf
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.