Manchmal stelle ich fest, dass Programme auf meinem Computer mit dem Fehler "reiner virtueller Funktionsaufruf" abstürzen.
Wie kompilieren diese Programme überhaupt, wenn ein Objekt nicht aus einer abstrakten Klasse erstellt werden kann?
Manchmal stelle ich fest, dass Programme auf meinem Computer mit dem Fehler "reiner virtueller Funktionsaufruf" abstürzen.
Wie kompilieren diese Programme überhaupt, wenn ein Objekt nicht aus einer abstrakten Klasse erstellt werden kann?
Antworten:
Sie können auftreten, wenn Sie versuchen, einen virtuellen Funktionsaufruf von einem Konstruktor oder Destruktor auszuführen. Da Sie keinen virtuellen Funktionsaufruf von einem Konstruktor oder Destruktor ausführen können (das abgeleitete Klassenobjekt wurde nicht erstellt oder wurde bereits zerstört), wird die Basisklassenversion aufgerufen, was im Fall einer reinen virtuellen Funktion nicht der Fall ist existiert nicht.
(Siehe Live-Demo hier )
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()
Aufruf im Konstruktor lässt sich leicht devirtualisieren und Base::doIt()
statisch weiterleiten , was nur einen Linkerfehler verursacht. Was wir wirklich brauchen, ist eine Situation, in der der dynamische Typ während eines dynamischen Versands der abstrakte Basistyp ist.
Base::Base
Rufen Sie eine nicht f()
virtuelle doIt
Methode auf, die wiederum die (reine) virtuelle Methode aufruft .
Neben dem Standardfall, eine virtuelle Funktion vom Konstruktor oder Destruktor eines Objekts mit rein virtuellen Funktionen aufzurufen, können Sie auch einen rein virtuellen Funktionsaufruf (zumindest unter MSVC) erhalten, wenn Sie eine virtuelle Funktion aufrufen, nachdem das Objekt zerstört wurde . Natürlich ist dies eine ziemlich schlechte Sache, aber wenn Sie mit abstrakten Klassen als Schnittstellen arbeiten und es vermasseln, ist es etwas, das Sie vielleicht sehen. Es ist möglicherweise wahrscheinlicher, wenn Sie referenzierte gezählte Schnittstellen verwenden und einen Ref-Count-Fehler haben oder wenn Sie eine Race-Bedingung für Objektverwendung / Objektzerstörung in einem Multithread-Programm haben ... Das Besondere an diesen Arten von Purecall ist, dass dies der Fall ist Oft ist es weniger einfach herauszufinden, was los ist, wenn die Überprüfung der "üblichen Verdächtigen" virtueller Anrufe in ctor und dtor sauber wird.
Um beim Debuggen dieser Art von Problemen zu helfen, können Sie in verschiedenen Versionen von MSVC den Purecall-Handler der Laufzeitbibliothek ersetzen. Sie tun dies, indem Sie Ihre eigene Funktion mit dieser Signatur versehen:
int __cdecl _purecall(void)
und verknüpfen Sie es, bevor Sie die Laufzeitbibliothek verknüpfen. Dies gibt Ihnen die Kontrolle darüber, was passiert, wenn ein Purecall erkannt wird. Sobald Sie die Kontrolle haben, können Sie etwas Nützlicheres als den Standard-Handler tun. Ich habe einen Handler, der einen Stack-Trace darüber liefern kann, wo der Purecall stattgefunden hat. Weitere Informationen finden Sie hier: http://www.lenholgate.com/blog/2006/01/purecall.html .
(Beachten Sie, dass Sie auch _set_purecall_handler () aufrufen können, um Ihren Handler in einigen Versionen von MSVC zu installieren.)
_purecall()
Aufruf, der normalerweise beim Aufrufen einer Methode einer gelöschten Instanz auftritt, erfolgt nicht , wenn die Basisklasse mit der __declspec(novtable)
Optimierung deklariert wurde (Microsoft-spezifisch). Damit ist es durchaus möglich, eine überschriebene virtuelle Methode aufzurufen, nachdem das Objekt gelöscht wurde, wodurch das Problem maskiert werden kann, bis es Sie in einer anderen Form beißt. Die _purecall()
Falle ist dein Freund!
Normalerweise, wenn Sie eine virtuelle Funktion über einen baumelnden Zeiger aufrufen - höchstwahrscheinlich wurde die Instanz bereits zerstört.
Es kann auch "kreativere" Gründe geben: Vielleicht haben Sie es geschafft, den Teil Ihres Objekts abzutrennen, in dem die virtuelle Funktion implementiert wurde. Normalerweise wurde die Instanz jedoch bereits zerstört.
Ich bin auf das Szenario gestoßen, dass die rein virtuellen Funktionen wegen zerstörter Objekte aufgerufen werden, Len Holgate
bereits eine sehr schöne Antwort haben , ich möchte mit einem Beispiel etwas Farbe hinzufügen:
Der Destruktor der abgeleiteten Klasse hat die vptr-Punkte auf die vtable der Basisklasse zurückgesetzt, die die rein virtuelle Funktion hat. Wenn wir also die virtuelle Funktion aufrufen, ruft sie tatsächlich die rein virutalen auf.
Dies kann aufgrund eines offensichtlichen Codefehlers oder eines komplizierten Szenarios mit Race-Bedingungen in Multithreading-Umgebungen geschehen.
Hier ist ein einfaches Beispiel (g ++ - Kompilierung mit deaktivierter Optimierung - ein einfaches Programm kann leicht entfernt werden):
#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();
}
Und die Stapelverfolgung sieht aus wie:
#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
Markieren:
Wenn das Objekt vollständig gelöscht ist, was bedeutet, dass der Destruktor aufgerufen wird und memroy zurückgefordert wird, erhalten wir möglicherweise einfach eine, Segmentation fault
da der Speicher zum Betriebssystem zurückgekehrt ist und das Programm einfach nicht darauf zugreifen kann. Dieses "rein virtuelle Funktionsaufruf" -Szenario tritt normalerweise auf, wenn das Objekt im Speicherpool zugewiesen wird, während ein Objekt gelöscht wird, der zugrunde liegende Speicher vom Betriebssystem tatsächlich nicht zurückgefordert wird und der Prozess dort immer noch darauf zugreifen kann.
Ich würde vermuten, dass aus irgendeinem internen Grund ein vtbl für die abstrakte Klasse erstellt wurde (es könnte für eine Art Laufzeit-Typ-Information erforderlich sein) und etwas schief geht und ein reales Objekt es bekommt. Es ist ein Fehler. Das allein sollte sagen, dass etwas, was nicht passieren kann, ist.
Reine Spekulation
edit: sieht so aus, als ob ich mich in dem fraglichen Fall irre. OTOH IIRC Einige Sprachen erlauben vtbl-Aufrufe aus dem Konstruktor-Destruktor.
Ich verwende VS2010 und wenn ich versuche, den Destruktor direkt von der öffentlichen Methode aus aufzurufen, wird zur Laufzeit der Fehler "Reiner virtueller Funktionsaufruf" angezeigt.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void SomeMethod1() { this->~Foo(); }; /* ERROR */
};
Also habe ich das, was sich in ~ Foo () befindet, verschoben, um die private Methode zu trennen, dann hat es wie ein Zauber funktioniert.
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void _MethodThatDestructs() {};
void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
Wenn Sie Borland / CodeGear / Embarcadero / Idera C ++ Builder verwenden, können Sie dies einfach implementieren
extern "C" void _RTLENTRY _pure_error_()
{
//_ErrorExit("Pure virtual function called");
throw Exception("Pure virtual function called");
}
Platzieren Sie beim Debuggen einen Haltepunkt im Code und sehen Sie den Aufrufstapel in der IDE. Andernfalls protokollieren Sie den Aufrufstapel in Ihrem Ausnahmehandler (oder dieser Funktion), wenn Sie über die entsprechenden Tools dafür verfügen. Ich persönlich benutze MadExcept dafür.
PS. Der ursprüngliche Funktionsaufruf befindet sich in [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp
Hier ist ein hinterhältiger Weg, wie es passieren kann. Das ist mir heute im Wesentlichen passiert.
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
offensichtlich nicht wahr, weil einfach falsch: Eine reine virtuelle Funktion wird nur aufgerufen, wenn callFoo()
sie innerhalb eines Konstruktors (oder Destruktors) aufgerufen wird, da sich das Objekt zu diesem Zeitpunkt noch (oder bereits) im Stadium A befindet. Hier ist eine laufende Version Ihres Codes ohne den Syntaxfehler in B b();
- die Klammern machen es zu einer Funktionsdeklaration, Sie möchten ein Objekt.