Dari mana asal error "panggilan fungsi virtual murni"?

106

Saya terkadang melihat program yang mogok di komputer saya dengan kesalahan: "panggilan fungsi virtual murni".

Bagaimana program-program ini bahkan dapat dikompilasi ketika sebuah objek tidak dapat dibuat dari kelas abstrak?

Brian R. Bondy
sumber

Jawaban:

107

Mereka dapat terjadi jika Anda mencoba membuat panggilan fungsi virtual dari konstruktor atau destruktor. Karena Anda tidak dapat membuat panggilan fungsi virtual dari konstruktor atau destruktor (objek kelas turunan belum dibuat atau telah dihancurkan), ia memanggil versi kelas dasar, yang dalam kasus fungsi virtual murni, tidak tidak ada.

(Lihat demo langsung di sini )

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
}
Adam Rosenfield
sumber
3
Adakah alasan mengapa kompiler tidak bisa menangkap ini, secara umum?
Thomas
21
Dalam kasus umum tidak dapat menangkapnya karena aliran dari ctor dapat pergi ke mana saja dan di mana saja dapat memanggil fungsi virtual murni. Ini Menghentikan Masalah 101.
shoosh
9
Jawabannya sedikit salah: fungsi virtual murni masih dapat didefinisikan, lihat Wikipedia untuk detailnya.
Frase yang
5
Saya pikir contoh ini terlalu sederhana: doIt()Panggilan di konstruktor mudah didevirtualisasi dan dikirim ke Base::doIt()statis, yang hanya menyebabkan kesalahan linker. Yang benar-benar kita butuhkan adalah situasi di mana tipe dinamis selama pengiriman dinamis adalah tipe dasar abstrak.
Kerrek SB
2
Ini dapat dipicu dengan MSVC jika Anda menambahkan tingkat tipuan ekstra: Base::Basepanggil non-virtual f()yang pada gilirannya memanggil doItmetode virtual (murni) .
Frerich Raabe
64

Selain kasus standar pemanggilan fungsi virtual dari konstruktor atau destruktor objek dengan fungsi virtual murni, Anda juga bisa mendapatkan panggilan fungsi virtual murni (setidaknya pada MSVC) jika Anda memanggil fungsi virtual setelah objek dihancurkan . Jelas ini adalah hal yang sangat buruk untuk dicoba dan dilakukan tetapi jika Anda bekerja dengan kelas abstrak sebagai antarmuka dan Anda mengacaukannya maka itu adalah sesuatu yang mungkin Anda lihat. Ini mungkin lebih mungkin jika Anda menggunakan antarmuka terhitung yang direferensikan dan Anda memiliki bug jumlah referensi atau jika Anda memiliki kondisi balapan penggunaan / penghancuran objek dalam program multi-utas ... Hal tentang purecall semacam ini adalah bahwa itu seringkali kurang mudah untuk memahami apa yang terjadi karena pemeriksaan 'tersangka biasa' dari panggilan virtual di ctor dan dtor akan tampil bersih.

Untuk membantu dengan debugging jenis masalah ini, Anda dapat, di berbagai versi MSVC, mengganti penangan purecall perpustakaan runtime. Anda melakukan ini dengan memberikan fungsi Anda sendiri dengan tanda tangan ini:

int __cdecl _purecall(void)

dan menautkannya sebelum Anda menautkan pustaka runtime. Ini memberi ANDA kendali atas apa yang terjadi ketika purecall terdeteksi. Setelah Anda memiliki kendali, Anda dapat melakukan sesuatu yang lebih berguna daripada penangan standar. Saya memiliki penangan yang dapat memberikan jejak tumpukan di mana purecall terjadi; lihat di sini: http://www.lenholgate.com/blog/2006/01/purecall.html untuk lebih jelasnya.

(Perhatikan, Anda juga dapat memanggil _set_purecall_handler () untuk menginstal penangan Anda di beberapa versi MSVC).

Len Holgate
sumber
1
Terima kasih atas petunjuk tentang mendapatkan pemanggilan _purecall () pada instance yang dihapus; Saya tidak menyadarinya, tetapi hanya membuktikannya pada diri saya sendiri dengan sedikit kode tes. Melihat dump postmortem di WinDbg saya pikir saya sedang berurusan dengan perlombaan di mana utas lain mencoba menggunakan objek turunan sebelum sepenuhnya dibangun, tetapi ini menyoroti masalah baru, dan tampaknya lebih sesuai dengan bukti.
Dave Ruske
1
Satu hal lagi yang akan saya tambahkan: _purecall()pemanggilan yang biasanya terjadi saat memanggil metode dari instance yang dihapus tidak akan terjadi jika kelas dasar telah dideklarasikan dengan __declspec(novtable)pengoptimalan (khusus Microsoft). Dengan itu, sangat mungkin untuk memanggil metode virtual yang diganti setelah objek dihapus, yang dapat menutupi masalah hingga menggigit Anda dalam bentuk lain. The _purecall()perangkap adalah teman Anda!
Dave Ruske
Itu berguna untuk mengetahui Dave, saya telah melihat beberapa situasi baru-baru ini di mana saya tidak mendapatkan purecall ketika saya pikir saya seharusnya. Mungkin saya telah melanggar optimasi itu.
Len Holgate
1
@LenHolgate: Jawaban yang sangat berharga. Ini TEPATNYA kasus masalah kami (kesalahan penghitungan ulang disebabkan oleh kondisi balapan). Terima kasih banyak karena telah mengarahkan kami ke arah yang benar (kami malah mencurigai adanya korupsi v-table dan menjadi gila mencoba menemukan kode pelakunya)
BlueStrat
7

Biasanya ketika Anda memanggil fungsi virtual melalui penunjuk yang menggantung - kemungkinan besar instance tersebut telah dimusnahkan.

Ada juga alasan yang lebih "kreatif": mungkin Anda telah berhasil memisahkan bagian dari objek tempat fungsi virtual diimplementasikan. Tetapi biasanya hanya saja instance tersebut telah dihancurkan.

Braden
sumber
4

Saya mengalami skenario bahwa fungsi virtual murni dipanggil karena objek yang hancur, Len Holgatesudah memiliki jawaban yang sangat bagus , saya ingin menambahkan beberapa warna dengan contoh:

  1. Objek Turunan dibuat, dan penunjuk (sebagai kelas Base) disimpan di suatu tempat
  2. Objek Turunan dihapus, tetapi entah bagaimana penunjuk masih direferensikan
  3. Pointer yang menunjuk ke objek Turunan yang dihapus dipanggil

Penghancur kelas turunan mengatur ulang poin vptr ke kelas dasar vtable, yang memiliki fungsi virtual murni, jadi ketika kita memanggil fungsi virtual, itu benar-benar memanggil ke virutal murni.

Ini dapat terjadi karena bug kode yang jelas, atau skenario rumit kondisi balapan di lingkungan multi-threading.

Berikut adalah contoh sederhana (kompilasi g ++ dengan pengoptimalan dinonaktifkan - program sederhana dapat dengan mudah dioptimalkan):

 #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();
 }

Dan jejak tumpukan terlihat seperti:

#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

Menyoroti:

jika objek dihapus sepenuhnya, yang berarti destruktor dipanggil, dan memroy diambil kembali, kita mungkin hanya mendapatkan a Segmentation faultkarena memori telah kembali ke sistem operasi, dan program tidak dapat mengaksesnya. Jadi skenario "panggilan fungsi virtual murni" ini biasanya terjadi ketika objek dialokasikan pada kumpulan memori, sementara objek dihapus, memori yang mendasari sebenarnya tidak diambil kembali oleh OS, itu masih dapat diakses oleh proses.

Baiyan Huang
sumber
0

Saya kira ada vtbl yang dibuat untuk kelas abstrak karena beberapa alasan internal (mungkin diperlukan untuk semacam info jenis waktu proses) dan ada yang tidak beres dan objek nyata mendapatkannya. Itu bug. Itu saja harus mengatakan bahwa sesuatu yang tidak dapat terjadi adalah.

Spekulasi murni

edit: sepertinya saya salah dalam kasus yang dimaksud. OTOH IIRC beberapa bahasa mengizinkan panggilan vtbl keluar dari destruktor konstruktor.

BCS
sumber
Ini bukan bug dalam kompilator, jika itu yang Anda maksud.
Thomas
Kecurigaan Anda benar - C # dan Java mengizinkan ini. Dalam bahasa-bahasa tersebut, proyek yang sedang dibangun memiliki tipe akhirnya. Di C ++, objek berubah tipe selama konstruksi dan itulah mengapa dan kapan Anda bisa memiliki objek dengan tipe abstrak.
MSalters
SEMUA kelas abstrak, dan objek nyata yang dibuat berasal darinya, memerlukan vtbl (tabel fungsi virtual), yang mencantumkan fungsi virtual mana yang harus dipanggil. Dalam C ++ sebuah objek bertanggung jawab untuk membuat anggotanya sendiri, termasuk tabel fungsi virtual. Konstruktor dipanggil dari kelas dasar ke kelas turunan, dan destruktor dipanggil dari kelas turunan ke kelas dasar, sehingga dalam kelas dasar abstrak tabel fungsi virtual belum tersedia.
fuzzyTew
0

Saya menggunakan VS2010 dan setiap kali saya mencoba memanggil destruktor langsung dari metode publik, saya mendapatkan kesalahan "panggilan fungsi virtual murni" selama runtime.

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void SomeMethod1() { this->~Foo(); }; /* ERROR */
};

Jadi saya memindahkan apa yang ada di dalam ~ Foo () ke metode privat terpisah, lalu itu bekerja seperti pesona.

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
David Lee
sumber
0

Jika Anda menggunakan Borland / CodeGear / Embarcadero / Idera C ++ Builder, Anda cukup mengimplementasikannya

extern "C" void _RTLENTRY _pure_error_()
{
    //_ErrorExit("Pure virtual function called");
    throw Exception("Pure virtual function called");
}

Saat proses debug, tempatkan breakpoint dalam kode dan lihat callstack di IDE, jika tidak, catat tumpukan panggilan di penangan pengecualian Anda (atau fungsi tersebut) jika Anda memiliki alat yang sesuai untuk itu. Saya pribadi menggunakan MadExcept untuk itu.

PS. Panggilan fungsi asli ada di [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp

Niki
sumber
-2

Inilah cara licik untuk mewujudkannya. Saya mengalami ini pada dasarnya terjadi pada saya hari ini.

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();
1800 INFORMASI
sumber
1
Setidaknya itu tidak dapat direproduksi pada vc2008 saya, vptr memang menunjuk ke vtabel A ketika pertama kali diinisialisasi di kontraktor A, tetapi kemudian ketika B sepenuhnya diinisialisasi, vptr diubah untuk menunjuk ke vtabel B, yang ok
Baiyan Huang
tidak dapat mereproduksinya dengan vs2010 / 12
makc
I had this essentially happen to me todayjelas tidak benar, karena salah: fungsi virtual murni dipanggil hanya ketika callFoo()dipanggil di dalam konstruktor (atau destruktor), karena saat ini objek masih (atau sudah) pada tahap A. Berikut adalah versi kode Anda yang sedang berjalan tanpa kesalahan sintaks di B b();- tanda kurung menjadikannya deklarasi fungsi, Anda menginginkan objek.
Serigala