it-swarm.com.de

Woher kommen Abstürze bei "rein virtuellen Funktionsaufrufen"?

Ich bemerke manchmal Programme, die auf meinem Computer mit dem Fehler abstürzen: "pure virtual function call".

Wie kompilieren diese Programme auch, wenn ein Objekt nicht aus einer abstrakten Klasse erstellt werden kann?

102
Brian R. Bondy

Sie können auftreten, wenn Sie versuchen, einen virtuellen Funktionsaufruf von einem Konstruktor oder Destruktor aus durchzuführen. Da Sie keinen virtuellen Funktionsaufruf von einem Konstruktor oder Destruktor aus durchführen können (das abgeleitete Klassenobjekt wurde noch nicht erstellt oder wurde bereits zerstört), ruft es die Basisklassenversion auf, die im Fall einer reinen virtuellen Funktion nicht funktioniert gibt es 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
}
104
Adam Rosenfield

Neben dem Standardfall des Aufrufs einer virtuellen Funktion aus dem Konstruktor oder Destruktor eines Objekts mit reinen virtuellen Funktionen können Sie auch einen reinen virtuellen Funktionsaufruf (zumindest in MSVC) erhalten, wenn Sie eine virtuelle Funktion aufrufen, nachdem das Objekt zerstört wurde . Offensichtlich 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 werden. Es ist wahrscheinlich wahrscheinlicher, wenn Sie referenzierte Counted-Interfaces verwenden und einen Ref Count-Fehler haben oder wenn Sie in einem Multithread-Programm eine Objektverwendungs-/Objektzerstörungs-Race-Bedingung haben Oft ist es weniger einfach herauszufinden, was passiert, wenn die "üblichen Verdächtigen" von virtuellen Anrufen in ctor und dtor überprüft werden.

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, bevor Sie die Laufzeitbibliothek verknüpfen. Auf diese Weise können Sie steuern, was passiert, wenn ein reiner Anruf erkannt wird. Sobald Sie die Kontrolle haben, können Sie etwas Nützlicheres tun als den Standard-Handler. Ich habe einen Handler, der einen Stack-Trace des PureCall-Ereignisses bereitstellen kann. 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.).

62
Len Holgate

Normalerweise, wenn Sie eine virtuelle Funktion über einen baumelnden Zeiger aufrufen - höchstwahrscheinlich wurde die Instanz bereits zerstört.

Es kann auch "kreative" Gründe geben: Vielleicht haben Sie es geschafft, den Teil Ihres Objekts zu entfernen, in dem die virtuelle Funktion implementiert wurde. In der Regel ist die Instanz jedoch bereits zerstört.

7
Braden

Ich bin auf das Szenario gestoßen, dass die rein virtuellen Funktionen wegen zerstörter Objekte aufgerufen werden, Len Holgate habe schon ein sehr schönes antwort , ich möchte ein paar farben mit einem beispiel hinzufügen:

  1. Ein abgeleitetes Objekt wird erstellt und der Zeiger (als Basisklasse) irgendwo gespeichert
  2. Das abgeleitete Objekt wird gelöscht, der Zeiger wird jedoch trotzdem referenziert
  3. Der Zeiger, der auf das gelöschte abgeleitete Objekt zeigt, wird aufgerufen

Der Destruktor für abgeleitete Klassen setzte die vptr-Punkte auf die vtable der Basisklasse zurück, die die reine virtuelle Funktion hat. Wenn wir also die virtuelle Funktion aufrufen, ruft er tatsächlich die rein virutalen auf.

Dies kann aufgrund eines offensichtlichen Codefehlers oder eines komplizierten Szenarios der Racebedingung in Multithreading-Umgebungen auftreten.

Hier ist ein einfaches Beispiel (g ++ kompilieren mit deaktivierter Optimierung - ein einfaches Programm könnte einfach wegoptimiert 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 der Stack-Trace sieht so aus:

#0  0x00007ffff7499428 in __GI_raise ([email protected]=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

Hervorheben:

wenn das Objekt vollständig gelöscht ist, was bedeutet, dass Destruktor aufgerufen und Memroy zurückgefordert wird, erhalten wir möglicherweise einfach ein Segmentation fault da der Speicher zum Betriebssystem zurückgekehrt ist und das Programm einfach nicht darauf zugreifen kann. Dieses "reine 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 nicht zurückgefordert wird und der Prozess weiterhin darauf zugreifen kann.

1
Baiyan Huang

Ich verwende VS2010 und wenn ich versuche, Destruktor direkt von einer öffentlichen Methode aufzurufen, erhalte ich zur Laufzeit den Fehler "Reine virtuelle Funktion aufrufen".

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

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

Also habe ich das, was in ~ Foo () enthalten ist, 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 */
};
0
David Lee

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 in dieser Funktion), wenn Sie über die entsprechenden Tools verfügen. Ich persönlich benutze dafür MadExcept.

PS. Der ursprüngliche Funktionsaufruf befindet sich in [C++ Builder]\source\cpprtl\Source\misc\pureerr.cpp

0
Niki

Ich schätze, es gibt eine vtbl, die aus irgendeinem internen Grund für die abstrakte Klasse erstellt wurde (sie wird möglicherweise für eine Art Laufzeit-Typ-Info benötigt), und etwas geht schief, und ein reales Objekt bekommt sie. Es ist ein Fehler. Das allein sollte heißen, 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 heraus.

0
BCS