Axel Rogat
Objektorientiertes Programmieren mit C++ und JAVA
 
18.3: Mehrfachv./Layering Kapitel 18 19.1: Ausnahmebehandlung 
 
  18.4 Virtuelle Basisklassen  
 

Als einfachstes motivierendes Beispiel bringen wir eine mögliche zusätzliche Eigenschaft unserer grafischen Objekte ins Spiel, nämlich ihre Farbe. Wir leiten dazu von der Basisklasse Shape die Klasse ColoredShape ab:

class ColoredShape : public Shape { private: unsigned int red,green,blue; public: ColoredShape(const Point2D &r) : Shape(r),red(0),green(0),blue(0) { } ColoredShape(const Point2D &rf, int r, int g, int b) : Shape(rf) { set_colors(r,g,b); } void set_colors(int r, int g, int b) { if (r>255 || g>255 || b>255) error("illegal component range"); red=r; green=g; blue=b; } void read_colors(int &r, int &g, int &b) const { r=red; g=green; b=blue; } };
Wie bei den meisten farbigen grafischen Systemen sollen die Farben durch Mischen aus den Grundfarben Rot, Grün und Blau entstehen. Systemabhängig wird es dafür spezielle Typen geben (RGBcolor o.ä.), wir arbeiten hier der Einfachheit halber mit drei int-Komponenten mit zulässigem Bereich von 0 bis 255 (daher machen wir sie vorsichtshalber private und stellen Schreib- und Lesefunktionen zur Verfügung).

Auch ein farbiges Objekt macht ohne weitere Eigenschaften keinen Sinn -- die Klasse ist wiederum nur zum Ableiten konkreterer Klassen gedacht. Beispielsweise wollen wir mit farbigen Rechtecken arbeiten. Bei einfacher Vererbung müßten wir von Rectangle ableiten und den Code für die Farbe duplizieren oder von ColoredShape ableiten und den Code für die Rechtecke duplizieren. Wir erledigen beides durch Ableiten von beiden Klassen:

class ColoredRectangle : public ColoredShape, public Rectangle { public: ColoredRectangle(const Point2D &p, int l1, int l2) : ColoredShape(p), Rectangle(p,l1,l2) { } ColoredRectangle(const Point2D &p, int l1, int l2, int r, int g, int b) : ColoredShape(p,r,g,b), Rectangle(p,l1,l2) { } };
Wenn wir die Ableitung in dieser Form durchführen, erhalten wir nicht die Objektstruktur, die wir uns eigentlich gewünscht haben, und wir bekommen deshalb auch mehrere Probleme.

Die Klassenhierarchie, die wir erzeugt haben, ist rechts dargestellt. Die Klasse Shape ist zweimal beim Ableitungsvorgang beteiligt -- durch die beiden Wege über ColoredShape und Rectangle. Ein Objekt vom Typ ColoredRectangle hat dementsprechend zwei anonyme Shape-Subobjekte, insbesondere zwei Referenzpunkte.

Erstens macht das wenig Sinn, und zweitens führt es zu Mehrdeutigkeiten. Die Memberfunktionen, die bereits in Shape definiert sind, wurden doppelt geerbt. Bei Verwendung etwa von moveto() wird jedesmal ein Fehler gemeldet.

Wenn wir unsere abstrakte Shape-Version als Grundlage verwenden, verschlimmert sich die Situation noch, weil ColoredRectangle abstrakt bleibt! Die rein virtuellen Funktionen wie area und rotate aus Shape werden in Rectangle zwar konkretisiert, nicht aber die, die über den ColoredShape-Weg Eingang in unsere Klasse finden. Dementsprechend können wir keine ColoredRectangle-Objekte erzeugen:

cannot declare variable `cr' to be of type `ColoredRectangle' since the following virtual functions are abstract: void Shape::realize(class Window &) const void Shape::rotate(double) double Shape::area() const
Was wir eigentlich erreichen wollten, war, daß die Eigenschaften der Klasse Shape auf zwei verschiedene Weisen ergänzt werden. Der Graph, den wir realisieren wollen, ist der DAG (directed acyclic graph), der rechts dargestellt ist. Die Stellen, an denen das in C++ eingeht, sind dabei die Deklarationen von ColoredShape und Polygon.

Basisklassen, die in dieser Art vererbt werden sollen, heißen virtuelle Basisklassen. Diese Bezeichnung ist deswegen gerechtfertigt, weil der Mechanismus zum Ansprechend des zugehörigen anonymen Subobjekts dem von virtuellen Funktionen sehr ähnlich ist.

In der Deklaration von ColoredShape und Polygon versehen wir dazu die Basisklassen-Angabe Shape mit dem zusätzlichen Schlüsselwort virtual (vor oder nach public):

class ColoredShape : public virtual Shape { /* ... */ }; class Polygon : public virtual Shape { /* ... */ };
Wir werden weiter unten sehen, warum wir bereits in Polygon ansetzen müssen und nicht etwa erst in Rectangle.

Bei der internen Darstellung von Objekten von Klassen mit einer virtuellen Basisklasse ergibt sich ein Problem, das auch direkte Auswirkungen auf den Code hat: Es darf in Rectangle nur ein Shape-Subobjekt geben, aber das muß sowohl zum ColoredShape-Subobjekt wie auch zum Polygon-Subobjekt gehören.

Auf den Code bezogen bedeutet das: Sowohl das ColoredShape-Subobjekt wie auch das Polygon-Subobjekt initialisieren ihr Shape-Subobjekt. Wenn die beiden Shapes zu einem zusammenfallen, wie wird dieses nun initialisiert -- der Reihe nach durch Überschreiben, durch die erste oder durch die letzte Klasse in der Initialisiererliste?

Intern wird zur Darstellung der Aufbau der Subobjekte verändert, in denen die Basisklasse als virtuell deklariert wurde. Statt des Subobjekts der Basisklasse findet man dort nur einen Pointer bptr auf das eigentliche Subobjekt, das an der Stelle des Gesamtobjekts untergebracht ist, an der im Graphen die beiden Äste wieder zusammenkommen.

In unserem Beispiel sieht ein ColoredRectangle-Objekt also in etwa wie folgt aus (der genaue Aufbau kann von Compiler zu Compiler variieren):

Wenn man einen Pointer auf ein ColoredRectangle-Objekt erzeugt und mit dynamischen Cast-Operationen darauf arbeitet, erhält man die tatsächlichen Adressen der einzelnen Subobjekte, z.B.:

ColoredRectangle cr(Point2D(5,5),10,20,96,128,192); cout << (long)(dynamic_cast<ColoredShape*>(&cr)) - (long)(&cr) << endl; cout << (long)(dynamic_cast<Polygon*>(&cr)) - (long)(&cr) << endl; cout << (long)(dynamic_cast<Rectangle*>(&cr)) - (long)(&cr) << endl; cout << (long)(dynamic_cast<Shape*>(&cr)) - (long)(&cr) << endl;
Beim GNU-Compiler auf einer SUN-Station erhalten wir hier die Ausgaben 0,24,24,40.

Hier sieht man auch, warum es nötig ist, die Virtualität in den Klassen ColoredShape und Polygon zu definieren: Die Pointer-Struktur darf nicht erst im letzten Schritt eingesetzt werden. Damit ein solcher Aufbau funktioniert, müssen alle Objekte dieser Klassen den verpointerten Aufbau haben, auch, wenn sie nicht als Subobjekte von ColoredRectangle verwendet werden.

Es bleibt noch das Problem der Initialisierung des mehrfach eingebetteten Subobjekts (bei uns Shape) zu lösen.

Um die Initialisierung des Basisklassen-Teilobjekts eindeutig zu machen, hat man folgendes festgelegt:

In unserem Fall müssen wir also explizit einen Shape-Konstruktor aufrufen -- in allen Klassen von Polygon ab abwärts. Polygon erbt direkt von Shape, so daß hier nichts zu ändern ist. Zusätzliche Aufrufe gehören aber nun nach Rectangle, Square und Triangle:

class Rectangle : public Polygon { public: Rectangle(Point2D p, int l1, int l2) : Shape(p), Polygon(4,p,p+Vector2D(l1,0),p+Vector2D(l1,l2),p+Vector2D(0,l2)) { } // ... };
Der Rectangle-Konstruktor ruft nun explizit Shape(p) auf. Außerdem wird zwar der Polygon-Konstruktor aufgerufen -- der dort enthaltene Aufruf des Shape-Konstruktors wird aber ignoriert!

Ähnlich sehen Square und Triangle aus:

class Square : public Rectangle { public: Square(Point2D p, double a) : Shape(p), Rectangle(p,p+Vector2D(a,a)) { } }; class Triangle : public Polygon { public: Triangle(const Point2D& p, const Point2D& q, const Point2D& r) : Shape(p), Polygon(3,p,q,r) { } // ... };
Bei ColoredRectangle wird der Shape-Konstruktor sogar dreimal angegeben (direkt, über ColoredShape und über Rectangle), aber nur einmal tatsächlich aufgerufen:
class ColoredRectangle : public ColoredShape, public Rectangle { public: ColoredRectangle(const Point2D &rf, int l1, int l2) : Shape(rf), ColoredShape(rf), Rectangle(rf,l1,l2) { } ColoredRectangle(const Point2D &rf, int l1, int l2, int r, int g, int b) : Shape(rf), ColoredShape(rf,r,g,b), Rectangle(rf,l1,l2) { } };
Beispiel: In Kapitel 16 mußten wir uns bei unseren Array-Klassen etwas behelfen.

Die Klasse checked_arith_array sollte die Index-Funktionalität von checked_array und die Summen-Funktion von arith_array bieten. Sie durfte aber nicht von beiden Klassen erben, da sie sonst nicht nur deren Funktionalität, sondern auch ihre Datenmember geerbt hätte -- insbesondere also zwei interne Arrays!

Zunächst müssen wir das Schlüsselwort virtual in der Definition der beiden Zwischenklassen einfügen (ansonsten wird bei ihnen nichts verändert):

template <class T> class checked_array : public virtual array<T> { ... }; template <class T> class arith_array : public virtual array<T> { ... };
Die Definition der neuen Klasse besteht jetzt nur noch aus der Umlenkung der Konstruktoren:
template <class T> class checked_arith_array<T> : public checked_array<T>, public arith_array<T> { public: checked_arith_array(int num) : array(num), checked_array<T>(0), arith_array<T>(0) { } checked_arith_array(int i1, int i2) : array(i1,i2), checked_array<T>(0), arith_array<T>(0) { } };
Wir sehen, daß das array-Subobjekt allein durch die array-Klausel in der Liste initialisiert wird. Für die direkten Vorgängerklassen sind hier Dummy-Konstruktoren angegeben, da sie keine zusätzlichen Datenmember liefern und ihr array-Teil ohnehin ignoriert wird. (Sie müssen aber angegeben werden, weil es keine Standard-Konstruktoren ihrer Klassen gibt.)

Zu beachten ist, daß der Index-Operator nicht zweimal geerbt wird. Es gibt zwar in beiden direkten Oberklassen eine Version von ihm, aber die aus arith_array ist aus array übernommen. Diese gilt als überschrieben durch die Version aus checked_array. Es wird also die gecheckte Version verwendet.

Nur, wenn auch in arith_array eine neue Version von [] definiert wäre, gäbe es Mehrdeutigkeiten. Der Operator müßte dann in checked_arith_array neu definiert werden (er könnte dann natürlich auf eine der Oberklassen-Versionen verweisen).

 
18.3: Mehrfachv./Layering Startseite 19.1: Ausnahmebehandlung 
 

© 1998 Axel Rogat