Polymorphie und heterogene Container

Martin Kompf

Polymorphie Die Speicherung von Objekten mit unterschiedlichem Typ in einem einzigen Container ist nicht trivial. Der Artikel zeigt die richtige Vorgehensweise.

Das Problem klingt zunächst einfach: In einem STL-Container, wie zum Beispiel list, sollen Objekte mit unterschiedlichem Typ (unterschiedlichen Klassen) gespeichert werden. Man bezeichnet einen solchen Container auch als »heterogen«. Nun handelt es sich bei den Containertypen der STL durchweg um Templates, die bei ihrer Deklaration die Angabe des Typs der enthaltenen Objekte verlangen. Wie kann man nun trotzdem Objekte unterschiedlichen Typs in einem einzigen Container speichern? Der Ausweg liegt in der Verwendung voneinander abgeleiteter Typen. Dann kann der Basistyp bei der Templateinstanzzierung angegeben werden, alle abgeleiteten Typen sind zu ihm kompatibel und lassen sich ebenfalls in den Container einfügen.

Das Beispiel

Zur Illustration soll ein Beispiel dienen: Ein Bildbearbeitungsprogramm soll eine Liste von Images unterschiedlichen Typs, zum Beispiel JPEG, PNG, GIF und so weiter, verwalten. Diese Liste soll als STL-Container vom Typ list realisiert werden. Dazu muss nun zunächst ein Basistyp für alle Images definiert werden. Dieser Basistyp soll etwas darstellen, was alle Images gemeinsam haben. Das könnte zum Beispiel die Eigenschaft sein, sich mittels einer Funktion display selbst anzuzeigen. Der Basistyp (die Basisklasse) Image wird dann folgendermaßen definiert:

class Image {
  public:
    virtual void display() {};
};

Von dieser Klasse können nun sämtliche konkreten Imagetypen abgeleitet werden, zum Beispiel JpegImage und PngImage:

class JpegImage : public Image {
  public:
    JpegImage(const string& name) : _name(name)
    {}

    virtual void display() {
      cout << "JPEG viewer: " << _name << endl;
    }
  private:
    string _name;
};


class PngImage : public Image {
  public:
    PngImage(const string& name) : _name(name)
    {}

    virtual void display() {
      cout << "PNG viewer: " << _name << endl;
    }
  private:
    string _name;
};

Polymorphie

Die display Funktionen ersetzen hier die leere Funktion des gleichen Namens aus der Basisklasse Image mit einer konkreten - vom Typ des Images abhängigen - Implementierung (die hier nur beispielhaft ausgeführt wurde). Das Ziel dieser Maßnahme ist, dass beim Durchlaufen aller Images der Liste jeweils die richtige display Funktion des konkreten Images aufgerufen wird. Dieses Verhalten bezeichnet man als Polymorphie. Damit sind die Vorbereitungen angeschlossen, eine konkrete Anwendung könnte nun so aussehen:

// Erzeuge die Liste
list<Image> imageList;

// Füge drei Images ein
Image img;
img = JpegImage("bild1.jpg");
imageList.push_back(img);
img = PngImage("bild2.png");
imageList.push_back(img);
img = JpegImage("bild3.jpg");
imageList.push_back(img);

// Zeige Images der Liste an
list<Image>::iterator it;
for (it = imageList.begin(); it != imageList.end(); ++it) {
  it->display();
}

Fehlversuch

Leider funktioniert dieser - durchaus logische - Algorithmus nicht wie gewünscht. Lassen wir das Programm ablaufen, so erscheint keine Ausgabe. Dieses Verhalten liegt in einer Eigenschaft von C++ begründet, die man salopp als »Polymorphie geht nicht mit Objekten - nur mit Pointern« formulieren könnte. Bei der Zuweisung

img = JpegImage(...);

geht nämlich die Information, dass es sich um ein Objekt vom Typ JpegImage handelt, völlig verloren. In der Liste stehen dann nur noch Objekte vom Typ Image, und deren display Funktion ist leer! Hätten wir übrigens Image als abstrakte Basisklasse definiert, dass heisst die Definition einer pur virtuellen Funktion

  virtual void display() = 0;

verwendet, dann hätte uns schon der Compiler warnen können:

Error E2352 C:\Programme\Borland\bcc55\include\list.h
110: Cannot create instance of abstract class 'Image'
in function main()

Zeiger müssen her

Das Problem lässt sich dadurch Umschiffen, dass in der Liste nicht die Objekte selbst, sondern nur Zeiger auf dieselben gespeichert werden. Zwar geht dann beim Einfügen der Zeiger in die Liste auch deren Typ verloren, aber der Typ des eigentlichen Objektes, auf welches gezeigt wird, bleibt unverändert:

// Erzeuge die Liste
list<Image *> imageList;

// Füge drei Images ein
Image *img;
img = new JpegImage("bild1.jpg");
imageList.push_back(img);
img = new PngImage("bild2.png");
imageList.push_back(img);
img = new JpegImage("bild3.jpg");
imageList.push_back(img);

// Zeige Images der Liste an
list<Image *>::iterator it;
for (it = imageList.begin(); it != imageList.end(); ++it) {
  (*it)->display();
}

Dieses Programm zeigt das gewünschte Verhalten, das heisst wir sehen jetzt die erwartete Ausgabe

JPEG viewer: bild1.jpg
PNG viewer: bild2.png
JPEG viewer: bild3.jpg

Speicherverschwendung

Ein Wermutstropfen bleibt jedoch noch: Wir haben die Imageobjekte per new auf dem Heap angelegt - wer gibt den allokierten Speicherbereich nun wieder frei? Die Idee, dass dies ja eigentlich der Container am Ende seiner Lebenszeit tun könnte, funktioniert leider nicht. Im Container stehen ja nur Pointer, nicht die Objekte selbst! Wir können dieses Verhalten überprüfen, indem wir jeder Klasse (auch Image!) einen virtuellen Destruktor hinzufügen, der irgendetwas ausgibt. Zum Beispiel:

  virtual ~JpegImage() {
      cout << "free " << _name << endl;
  }

Diese Ausgaben sind nie zu sehen; wie befürchtet wird der den Imageobjekten zugewiesene Speicher nie freigegeben.

Die Lösung: Smarte Zeiger

Einen Ausweg aus diesem Dilemma können nur »Smart« Pointer mit eingebautem Reference Counting bieten. Leider sind diese (noch) nicht im C++ Standard enthalten, man muss auf Implementierungen, wie die aus Boost C++ oder die vom Autor im Artikel Reference Counting vorgestellte, zurückgreifen. Mit letzterer sieht unser Algorithmus nun folgendermaßen aus:

#include "refcnt_ptr.h"

// ...

typedef refcnt_ptr<Image> ImgPtr;
// Erzeuge die Liste
list<ImgPtr> imageList;

// Füge drei Images ein
Image *img;
img = new JpegImage("bild1.jpg");
imageList.push_back(ImgPtr(img));
img = new PngImage("bild2.png");
imageList.push_back(ImgPtr(img));
img = new JpegImage("bild3.jpg");
imageList.push_back(ImgPtr(img));

// Zeige Images der Liste an
list<ImgPtr>::iterator it;
for (it = imageList.begin(); it != imageList.end(); ++it) {
  (*it)->display();
}

Die Programmausgabe ist jetzt

JPEG viewer: bild1.jpg
PNG viewer: bild2.png
JPEG viewer: bild3.jpg
free bild1.jpg
free bild2.png
free bild3.jpg

Fazit

Obwohl wir letztendlich - unter Zuhilfenahme nicht standardisierter »Smart« Pointer - eine funktionierende Lösung finden konnten, ist die Situation insgesamt nicht zufriedenstellend. Die Implementierung des nicht so seltenen Problems »Heterogener Container« verlangt eine Menge von Detailwissen über die interne Funktionsweise von C++ und erfordert zusätzlichen Aufwand bei der Programmierung. Letztlich führten diese und ähnliche Fallen in C++ zur Entwicklung einfacher zu lernender und robusterer Sprachen, wie zum Beispiel Java. Ob diese auf lange Sicht C++ ablösen werden, bleibt abzuwarten.