CCD

Social Bookmarking

Durch Klick auf den nachfolgenden Button können Sie diese Seite bequem bei vielen Bookmark-Diensten und Social Websites anmelden bzw. bookmarken:

Publicons

Lizenz

Creative Commons License
Pure virtual function call PDF Drucken E-Mail
Freitag, den 09. April 2010 um 14:37 Uhr

"R6025 - Pure virtual function call" war die letzte Meldung des gerade abgestürzten und bis zu diesem Zeitpunkt anscheinend fehlerfrei laufenden Programms. In diesem Blog werde ich mögliche Ursachen für dieses Problem aufzeigen und Tipps zur Fehlersuche geben.

Die Fehlermeldung "Pure virtual function call" sagt exakt das aus, was auch tatsächlich passiert ist: es ist versucht worden, eine rein virtuelle Funktion, genauer: Methode, einer Klasse aufzurufen. Zur Erinnerung: eine virtuelle Methode ist eine Methode einer Klasse, deren Einsprungadresse erst zur Laufzeit ermittelt wird (sog. late binding). Bei den rein virtuellen Methoden in C++ setzt man diese Methoden explizit gleich null, wodurch die Klasse automatisch zu einer abstrakten Klasse wird, d.h. es können von ihr keine Instanzen erzeugt werden. Hier ein Beispiel:

class AbstractClass
{
public:
  AbstractClass(void) { };
  virtual ~AbstractClass(void) { };

protected:
  virtual bool thePureVirtualFunction(void) = 0;
};

Würde man nun versuchen, von der Klasse AbstractClass ein Objekt zu erzeugen, so würde der Compiler die Übersetzung mit einer Fehlermeldung abbrechen:

1>.\main.cpp(5) : error C2259: 'AbstractClass' : cannot instantiate abstract class
1> due to following members:
1> 'bool AbstractClass::thePureVirtualFunction(void)' : is abstract

Der designierte Sinn und Zweck von abstrakten Klassen ist es ja, die Rolle einer Basisklasse einzunehmen und in einer Vererbungshierarchie grundlegende Eigenschaften von Unterklassen festzulegen. Entwickler müssen daher in den Klassen, die von einer abstrakten Klasse abgeleitet sind, alle vererbten abstrakten Methoden überschrieben und implementieren, damit die erbende Klasse selbst nicht abstrakt ist:

class Base
{
public:
  Base(void) { };
  virtual ~Base(void) { };

protected:
  virtual bool thePureVirtualFunction(void) = 0;
};

class Derived : public Base
{
public:
  Derived(void) { };
  virtual ~Derived(void) { };

protected:
  virtual bool thePureVirtualFunction(void)
  {
    // Do expedient things here...
    return true;
  };
};

Versuchen wir nun irgendwo in unserem Programm ein Objekt der Klasse Derived zu instanziieren, so ist das nun kein Problem mehr, denn thePureVirtualFunction aus Base wurde in Derived überschrieben und implementiert:

int main(int argc, char *argv[])
{
  Derived obj; // OK!
  return 0;
};

So weit, so gut. Kommen wir aber nun wieder zurück zu unserem eigentlichen Problem, dem "pure virtual function call".

Als erstes werden wir nun unser vorheriges Beispiel modifizieren und versuchen, in dem Basisklassen-Konstruktor von Base die rein virtuelle Methode thePureVirtualFunction direkt aufzurufen:

class Base
{
public:
  // Attempt to call the pure virtual function directly here...
  Base(void) { thePureVirtualFunction(); };

  // ...remaining code as in the previous code sample...
};

Was nun passiert, ist abhängig vom Compiler. Das Microsoft Visual-Studio 2008 beispielsweise meldet einen Linker-Fehler für thePureVirtualFunction:

error LNK2001: unresolved external symbol

Der Microsoft-Compiler ignoriert in diesem Fall jegliche Polymorphie zur Laufzeit und behandelt den Aufruf von thePureVirtualFunction wie einen statischen Aufruf der Basisklassenmethode. Da diese in Base aber noch nicht implementiert ist, schlägt das anschließende, statische linken fehl.

Den gleichen Linker-Fehler erhalten wir, wenn wir den Aufruf im Destruktor von Base platzieren:

virtual ~Base(void) { thePureVirtualFunction(); };

Auf diese Art und Weise können wir anscheinend den Fehler zur Laufzeit nicht provozieren, da schon der Compiler bzw. Linker hier einen Fehler erkennt.

Versuchen wir stattdessen doch nun einmal, die virtuelle Methode indirekt aufzurufen, indem wir im Konstruktor von Base zunächst eine nicht-virtuelle Methode doSomething aufrufen, die ihrerseits thePureVirtualFunction aufruft:

class Base
{
public:
  Base(void) { doSomething(); };
  virtual ~Base(void) { };

protected:
  void doSomething(void)
  {
    thePureVirtualFunction();
  };
  virtual bool thePureVirtualFunction(void) = 0;
};

class Derived : public Base
{
public:
  Derived(void) { };
  virtual ~Derived(void) { };

protected:
  virtual bool thePureVirtualFunction(void)
  {
    // Do expedient things here...
    return true;
  };
};

int main(int argc, char *argv[])
{
  Derived obj; // ???
  return 0;
};

In diesem Fall dürften Compiler und Linker keine Probleme haben, das Programm erfolgreich zu bauen. Führen wir das Programm allerdings anschließend aus, so werden wir sofort mit einem Fehler "pure virtual function called" (o.ä., je nach Compiler) konfrontiert. Dasselbe passiert, wenn wir den indirekten Aufruf der virtuellen Methode via doSomething im Destruktor von Base platzieren.

Dieses Beispiel zeigt: eine Ursache für diesen Fehler in Programmen können also indirekte Aufrufe virtueller Funktionen aus Konstruktoren oder Destruktoren sein, die man unbedingt vermeiden sollte!

„Never call virtual functions during construction or destruction.“
(Scott Meyers, Effective C++, 3rd Edition, Item No. 9)

Baumelnde Zeiger

Es existiert eine weitere, potenzielle Möglichkeit, mit einem Fehler "pure virtual function called" konfrontiert zu werden, und das ist - wie so häufig - der sorglose Umgang mit Zeigern (Pointer). Um dieses zu demonstrieren, müssen wir unsere beiden Klassen aus dem vorherigen Beispiel wieder ein klein wenig modifizieren und mit ein paar Ausgaben auf stdout dekorieren:

#include <string>
#include <iostream>

class Base
{
protected:
  Base(void) { std::cout << "Base c'tor called." << std::endl; };

public:
  virtual ~Base(void) { };

  void doSomething(void)
  {
    std::cout << "Base::doSomething called." << std::endl;
    thePureVirtual();
  };

protected:
  virtual bool thePureVirtual(void) const = 0;
};

class Derived : public Base
{
public:
  Derived(void) { std::cout << "Derived c'tor called." << std::endl; };
  virtual ~Derived(void) { std::cout <<
    "Derived d'tor called." << std::endl; };

protected:
  virtual bool thePureVirtual(void) const
  {
    std::cout << "Derived::thePureVirtual called." << std::endl;
    return true;
  };
};

Auch die main-Funktion wird ein wenig verändert - wir machen ganz bewusst ein fehlerhaftes Programm daraus:

  1. int main(int argc, char *argv[])
  2. {
  3.   Base* p1 = new Derived();
  4.   std::cout << "Using pointer p1..." << std::endl;
  5.   p1->doSomething();
  6.   Base* p2 = p1;  // Need a copy of the pointer
  7.   delete p1;
  8.   std::cout << "Using pointer p2..." << std::endl;
  9.   p2->doSomething()// Uh oh!!
  10.  
  11.   return 0;
  12. };

Bis zu Zeile 6 ist noch alles in Ordnung. Schaut man sich die Pointer in einem Debugger an, so zeigen beide nach der Zuweisung auf dasselbe, gültige Objekt, welches seinerseits eine virtuelle Funktionstabelle (englisch: virtual function table, kurz auch vtbl) besitzt. In dieser virtuellen Funktionstabelle, die nichts anderes ist als eine Tabelle von Methodenzeigern, sieht man dann sowohl den virtuellen Destruktor, als auch die rein virtuelle Funktion thePureVirtual von Derived.

Das Problem entsteht mit dem Zerstören von Derived in Zeile 7. Je nach Compiler und Laufzeitumgebung wird p1 durch die delete-Operation für ungültig erklärt, manchmal weisen anschließend auch Entwickler einem solchen Zeiger explizit einen aussagekräftigen Wert zu, z.B. NULL. Pointer p2 aber zeigt nach wie vor auf die Addresse im Speicher, an dem sich vorher noch das Objekt befand. p2 ist ein sog. dangling pointer (deutsch: herumbaumelnder Zeiger).

Wie es nun an der Adresse im Speicher aussieht, auf die p2 noch immer zeigt, kann nicht gesagt werden. Auch was nun in Zeile 9 passiert, wo der Zeiger dereferenziert und die Methode doSomething aufgerufen wird, ist entsprechend dem C++-Standard nicht definiert, d.h. in der Praxis kann alles mögliche passieren! Eventuell hat das Memory-Management des Betriebssystem den Speicherbereich, auf den p2 zeigt, schon wieder in Benutzung. Manchmal werden solche Bereiche mit bestimmten Werten gefüllt, um sie als ungültig zu markieren, z.B. mit 0xfeeefeee oder 0xdeadbeef (dead beef = "totes Fleisch").

Es kann aber auch sein, das der Speicherbereich noch immer in exakt denselben Zustand ist wie vor dem delete! Mit anderen Worten: in einem solchen Fall zeigt p2 auf einen Speicherbereich, dessen Binärlayout einer Instanz vom Typ Base entspricht, mitsamt einer vermeintlichen virtuellen Funktionstabelle. Das Resultat des Methodenaufrufs in Zeile 9 wäre unter derartigen Umständen ein Fehler "pure virtual function call".

Tipps zu Fehlersuche

Der Aufruf einer rein virtuellen Funktion ist ein Programmierfehler! Daher kommt man nicht umhin, den Aufruf der rein virtuellen Funktion zu suchen und den Code dort so zu schreiben, dass sie nicht mehr aufgerufen wird.

Eine Möglichkeit den unzulässigen Aufruf zu finden, ist, rein virtuelle Funktionen vorübergehend durch virtuelle Funktionen mit einer Implementierung zu ersetzen, welche das Programm kontrolliert anhält und Zugriff auf den call stack (Aufrufreihenfolge) gewährt. Unter Windows bzw. Visual C++ kann man dazu beispielsweise die Windows API-Funktion DebugBreak (Header: Windows.h) aufrufen:

#include <Windows.h>

class Base
{
// ...
protected:
  virtual bool thePureVirtual(void) const
  {
    DebugBreak();
    return false;
  };
// ...
};

Eine weitere Möglichkeit im Debugger an den call stack zu kommen, ist das Setzen eines Haltepunkts (break point) in der Funktion _purecall (Visual C++) bzw. in __cxa_pure_virtual (gcc). Bei der Verwendung des Microsoft Visual-Studio findet man die Funktion _purecall in der Datei purevirt.c, bei der Verwendung des gcc befindet sich __cxa_pure_virtual normalerweise in der libstdc++.(a,so).

 

Kommentar schreiben


Sicherheitscode
Aktualisieren