Woher kommen Abstürze beim „reinen virtuellen Funktionsaufruf“?

106

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?

Brian R. Bondy
quelle

Antworten:

107

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
}
Adam Rosenfield
quelle
3
Gibt es einen Grund, warum der Compiler dies im Allgemeinen nicht abfangen konnte?
Thomas
21
Im allgemeinen Fall kann es nicht abgefangen werden, da der Fluss vom ctor überall hingehen kann und überall die reine virtuelle Funktion aufrufen kann. Dies ist Halting Problem 101.
Shoosh
9
Die Antwort ist leicht falsch: Möglicherweise ist noch eine reine virtuelle Funktion definiert. Weitere Informationen finden Sie in Wikipedia. Richtige Formulierung: möglicherweise nicht vorhanden
MSalters
5
Ich halte dieses Beispiel für zu simpel: Der 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.
Kerrek SB
2
Dies kann mit MSVC ausgelöst werden, wenn Sie eine zusätzliche Indirektionsebene hinzufügen: Base::BaseRufen Sie eine nicht f()virtuelle doItMethode auf, die wiederum die (reine) virtuelle Methode aufruft .
Frerich Raabe
64

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

Len Holgate
quelle
1
Vielen Dank für den Hinweis zum Abrufen eines _purecall () -Aufrufs für eine gelöschte Instanz. Das war mir nicht bewusst, aber ich habe es mir nur mit einem kleinen Testcode bewiesen. Als ich mir einen Postmortem-Dump in WinDbg ansah, dachte ich, ich hätte es mit einem Rennen zu tun, bei dem ein anderer Thread versuchte, ein abgeleitetes Objekt zu verwenden, bevor es vollständig erstellt wurde, aber dies wirft ein neues Licht auf das Problem und scheint besser zu den Beweisen zu passen.
Dave Ruske
1
Eine andere Sache, die ich hinzufügen möchte: Der _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!
Dave Ruske
Es ist nützlich, Dave zu kennen. Ich habe in letzter Zeit einige Situationen gesehen, in denen ich keine Purecalls bekam, als ich dachte, ich sollte es sein. Vielleicht habe ich diese Optimierung verfehlt.
Len Holgate
1
@ LenHolgate: Extrem wertvolle Antwort. Dies war genau unser Problemfall (falsche Nachzählung aufgrund der Rennbedingungen). Vielen Dank, dass Sie uns in die richtige Richtung
gelenkt haben
7

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.

Braden
quelle
4

Ich bin auf das Szenario gestoßen, dass die rein virtuellen Funktionen wegen zerstörter Objekte aufgerufen werden, Len Holgatebereits eine sehr schöne Antwort haben , ich möchte mit einem Beispiel etwas Farbe hinzufügen:

  1. Ein abgeleitetes Objekt wird erstellt und der Zeiger (als Basisklasse) wird irgendwo gespeichert
  2. Das abgeleitete Objekt wird gelöscht, aber irgendwie wird immer noch auf den Zeiger verwiesen
  3. Der Zeiger, der auf ein gelöschtes abgeleitetes Objekt zeigt, wird aufgerufen

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

Baiyan Huang
quelle
0

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.

BCS
quelle
Es ist kein Fehler im Compiler, wenn Sie das meinen.
Thomas
Ihr Verdacht ist richtig - C # und Java erlauben dies. In diesen Sprachen haben im Bau befindliche Objekte ihren endgültigen Typ. In C ++ ändern Objekte während der Erstellung ihren Typ. Deshalb und wann können Sie Objekte mit einem abstrakten Typ haben.
MSalters
ALLE abstrakten Klassen und daraus abgeleitete reale Objekte benötigen eine vtbl (virtuelle Funktionstabelle), in der aufgeführt ist, welche virtuellen Funktionen darauf aufgerufen werden sollen. In C ++ ist ein Objekt für die Erstellung eigener Mitglieder verantwortlich, einschließlich der virtuellen Funktionstabelle. Konstruktoren werden von der Basisklasse zur abgeleiteten und Destruktoren von der abgeleiteten zur Basisklasse aufgerufen, sodass in einer abstrakten Basisklasse die virtuelle Funktionstabelle noch nicht verfügbar ist.
FuzzyTew
0

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 */
};
David Lee
quelle
0

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

Niki
quelle
-2

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();
1800 INFORMATIONEN
quelle
1
Zumindest kann es auf meinem vc2008 nicht reproduziert werden. Der vptr zeigt auf die vtable von A, wenn er zum ersten Mal im Konstruktor von A initialisiert wird. Wenn B jedoch vollständig initialisiert ist, wird der vptr so geändert, dass er auf die vtable von B zeigt, was in Ordnung ist
Baiyan Huang
kann es auch nicht mit vs2010 / 12
reproduzieren
I had this essentially happen to me todayoffensichtlich 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.
Wolf