Vererbung und objektorientiertes Design – Tip 34 “Unterscheiden Sie zwischen der Vererbung der Schnittstelle und der Implementierung”

Meyers gibt dem Programmierer in Tip 34 Merksätze an die Hand, was es bedeutet, rein virtuell, einfach virtuell und nicht virtuell zu vererben.

Wird eine Funktion rein virtuell vererbt, vererbt sie eine Schnittstelle. Die abgeleitete Klasse muss diese Funktion implementieren.

Wird einfach virtuell vererbt, erbt die abgeleitete Klasse eine Schnittstelle und eine Standardimplementierung. Wird die Implementierung in der abgeleiteten Klasse nicht spezialisiert, wird implizit auf die Standardimplementierung zurück gegriffen. Dabei muss man sich der Gefahr bewusst sein, dass solch eine Spezialisierung vergessen werden kann. Es gibt zwei Möglichkeiten, um zu erzwingen, dass die Standardimplementierung explizit benutzt wird. Die vererbende Klasse kann die zu vererbende Methode rein virtuell deklarieren und die Standardimplementierung dann als protected implementieren. Die erbende Klasse muss dann eine Implementierung schreiben und kann inline die geschützte Implementierung explizit aufrufen. Oder die Basisklasse implementiert direkt die rein virtuelle Methode. Die Implementierung ist dann allerdings öffentlich. Sie muss dann auch wie bei der vorangehenden Methode von der Basisklasse explizit aufgerufen werden.

Schließlich bedeutet nicht virtuell zu vererben, dass man eine Schnittstelle und eine verbindliche Implementierung vererbt. Die Basisklasse legt damit eine Invariante fest, die durch die abgeleitete Klasse tunlichst nicht neu definiert werden sollte.

Vererbung und objektorientiertes Design – Tip 32 “Modellieren Sie bei der öffentlichen Vererbung die Beziehung >ist ein<“

Das Open Closed Principle (OCP), welches zum Herz der objektorientierten Programmierung gehört, wird durch Abstraktion und Vererbung implementiert. Es besagt, dass Code offen für Erweiterung und geschlossen für Veränderung sein soll. Anstatt funktionierenden Code zu ändern, wird für ein neues Feature Code hinzugefügt.

Wenn z.B. eine Funktion geometrische Formen auf den Bildschirm malt und zuerst auf den Typ der Form prüfen muss, um die entsprechende spezielle Malfunktion aufzurufen, widerspricht dies dem OCP. Für jede neue geometrische Form, z.B. Kreis, Rechteck oder Ellipse, muss die Funktion angefasst und geändert werden. Gibt es noch andere Funktionen, wie die Berechnung des Flächeninhalts, gibt es schon zwei Stellen, die geändert werden müssen, um einen neuen Typ hinzuzufügen.

Das OCP wird hingegen beherzigt, indem ein Interface Shape erstellt wird, welches die abstrakten Methoden Draw und ComputeArea besitzt. Konkrete Klassen wie Ellipse und Circle implementieren nun diese Methoden. Eine Funktion, um verschiedene geometrische Formen der Basisklasse Shape zu malen, ruft nun für jedes Objekt die Methode Draw über das Interface auf. Wird eine neue Form ins Leben gerufen, braucht die Funktion nicht geändert zu werden. Stattdessen muss die neue Form das Interface implementieren.
Nun gibt es wiederum ein Prinzip, um diese öffentliche Vererbung zu regeln, das Liskov Substitution Principle (LSP):

Funktionen, die Zeiger auf Basisklassen benutzen, müssen in der Lage sein, Objekte von abgeleiteten Klassen zu benutzen, ohne sie zu kennen. Basisklassenzeiger müssen also durch Zeiger auf abgeleitete Objekte substituiert werden können.

Daraus folgt, dass abgeleitete Objekte sich gleich wie Basisobjekte verhalten müssen.

Zurück zu Effektiv C++ programmieren, Tip 32 Modellieren Sie bei der öffentlichen Vererbung die Beziehung “ist ein”. So geht denn C++ hin und kompiliert es, wenn Zeiger auf abgeleitete Objekte anstelle von Basisklassenzeigern verwendet werden. Resultat: Bei Ableitung muss eine “ist ein”-Beziehung mit einkalkuliert werden. Was “ist ein” bedeutet wird jedoch anhand des prominenten Beispiels Rechteck und Quadrat deutlich. Umgangssprachlich ist ein Quadrat ein Rechteck. Es verhält sich aber nicht so. Und “ist ein” bedeutet in der objektorientierten Programmierung “verhält sich wie”. Leite ich ein Quadrat vom Rechteck ab, möchte ich auch die Breite des Rechtecks ändern können, ohne die Höhe ändern zu müssen. Um den Quadrat genüge zu tun, muss ich aber beides ändern.

Dass öffentliche Vererbung “ist ein” bedeutet, gebietet also die objektorientierte Programmierung und wird durch C++ mit dem entsprechenden freizügigen Kompilierverhalten unterstützt.

Um LSP C++-seitig ferner zu garantieren, soll man keine öffentlich geerbten Namen verdecken (Tip 34), denn die abgeleitete Klasse würde sich anders verhalten als die Basisklasse.
Wenn z.B. in der Basisklasse B die Methode mf1() rein virtuell ist und die Methode mf1(int) einfach virtuell, überdeckt die Neudeklaration von mf1() in der öffentlich abgeleiteten Klasse den Namen mf1 und mf1(int) wird nicht geerbt. Deswegen ist man gezwungen, in der abgeleiteten Klasse in der public-Deklaration mf1(int) mit using B::mf1 sichtbar zu machen.

Bei privater Vererbung jedoch muss nicht alles geerbt werden.

Design und Deklaration

Die Tips Erleichtern Sie die korrekte und erschweren Sie die falsche Verwendung von Schnittstellen und Behandeln Sie Klassendesign als Typdesign haben meines Erachtens wenig mit C++ zu tun. Höchstens, dass man sich desto mehr über korrekte Verwendung des entwickelten Codes Gedanken machen muss, je komplexer die Sprache ist. Und die Ausdrucksmöglichkeiten von C++ suchen Ihresgleichen.

Auf Tip 20 Übergeben Sie Objekte lieber als Verweis auf const statt als Wert wird im Buch sehr oft als der Tip verwiesen, der die Leistungsfähigkeit von C++-Code entscheidend erhöht. Wenn man ein Objekt nämlich als Zeiger oder Referenz übergibt, müssen Kopierkonstruktor und Destruktor nicht bemüht werden. Und je nach Größe des Objekts und Häufigkeit der Übergabe, kann sich das sehr verflüssigend auf die Performance des Programms auswirken. Im Übrigen umgeht man bei Übergabe als Referenz dem Slicing-Problem. Wird ein Objekt einer abgeleiteten Klasse einer Funktion übergeben, die ein Objekt der Basisklasse entgegennimmt, wird innerhalb der Funktion auch nur ein Basisklassenobjekt konstruiert.
Anhand einer selbstgebastelten Klasse Rational erklärt Meyers aber, dass manchmal eine Übergabe als Objekt stattfinden muss. Wenn operator* ein Verweis auf ein lokal erzeugtes Objekt zurückgeben würde, würde dieses Objekt verrottet sein, wenn es die Funktion verlässt. Ein Zeiger auf ein auf dem Heap erstelltes Objekt kann ebenfalls nicht zurückgegeben werden, da eine Kette von Multiplikationen Speicherlöcher hervorrufen würde. Es gibt keine andere Alternative, als ein Objekt zurückzugeben.

Meyers Tip 22, Datenelemente private zu deklarieren ist Richtlinie in der objektorientierten Programmierung und hat nichts mit C++ zu tun. Solange man sich entscheidet, eine Klasse nicht als Datenstruktur zu programmieren, die ihre Datenelemente direkt öffentlich zur Verfügung stellt, sollte man die Implementierung von der Öffentlichkeit abkapseln, indem man den Zutritt zu den Datenelementen sperrt.

Tip 23, obgleich es auch hier um Objektorientiertheit geht, ist ein C++ Tip. Man soll nichtbefreundete Nichtelementfunktionen Elementfunktionen vorziehen. In C++ gibt es im Gegensatz zu C# oder Java globale Funktionen. Diese Eigenschaft lässt sich prima ausnutzen, um die Kapselung von Klassen zu erhöhen. Was hat eine Komforfunktion, die aus einer Sammlung von Aufrufen öffentlicher Methoden einer Klasse besteht, in der Klasse zu suchen? Um die Kohäsion, also den Zusammenhalt, einer Klasse zu erhöhen, sollten möglichst viele Methoden auf möglichst viele Datenelemente zugreifen. Komfortunktionen erhalten aber Zugriff auf private Elemente, den sie nicht benötigen.

Ressourcenverwaltung

Eine Ressource kann ein auf dem Heap angelegtes Objekt sein, dass nach Benutzung wieder gelöscht werden muss. Genauso müssen eine Datenbankverbindung oder ein Mutex vor Gebrauch gesperrt und nach Gebrauch wieder freigegeben werden. Zum Umgang mit Ressourcen empfiehlt sich das RAII (Resource Acquisition Is Initialization)-Prinzip. Gemäß diesem Prinzip wird eine angeforderte Ressource sofort bei Initialisierung in ein Objekt verpackt. Der Destruktor des Objekts kann dann zuverlässig garantieren, dass die Freigabe des Objekts korrekt geschieht. Zwei Standardklassen für die Ressourcenverwaltung sind std::tr1::auto_ptr und std::tr1::shared_ptr. Ein tr1::shared_ptr ist ein RCSP (Reference-Counting Smart Pointer), ein intelligenter Zeiger, der überwacht, wie viele Objekte auf eine Ressource zeigen, und automatisch diese Ressource löschen kann. tr1::shared_ptr kann bei Initiasierung auch eine Funktion mitgegeben werden, die aufgerufen wird, statt die Ressource zu löschen.

Beim Arbeiten mit Ressourcen verwaltenden Objekten ist auf richtiges Kopierverhalten zu achten. Im Allgemeinen sollte das Kopieren verhindert werden.

Konstruktoren, Destruktoren und Zuweisungsoperatoren

Man sieht schon, ich finde in diesem Buch wirklich jeden Satz lesenswert und jedes Detail wichtig. Nur wäre es nicht besonders effizient in bisheriger Detaildichte über das Buch zu berichten, deswegen ab jetzt kapitelweise.

Nett, dass angehender Vollprofi – für diese Leserschaft ist das Buch gedacht – noch kennenlernen darf, welche Funktionen C++ implizit generiert und aufruft, nämlich Standardkonstruktor, Kopierkonstruktor, Destruktor und Kopierzuweisungsoperator.

Um zu verhindern, dass solche Funktionen erzeugt werden, z.B. weil man ein Objekt nicht kopierbar machen möchte, deklariert man die entsprechenden Funktionen als private. Um das Kopieren zu verhindern sind dies Kopierkonstruktor und Kopierzuweisungsoperator. Bei fehlerhafter Verwendung erhält man einen Linkerfehler, da kein Code vorhanden ist, der die Deklaration definiert. Da bevorzugt wird, den Fehler so früh wie möglich zur Kompilierzeit aufzudecken, können die privaten Deklarationen in eine Basisklasse verschoben werden. Dies funktioniert dadurch, dass die implizit vom Compiler generierten Funktionen versuchen, ihre Gegenstücke in der Basisklasse aufzurufen.

Ein sehr nützlicher Tip sagt, dass man Destruktoren in polymorphen Basisklassen immer virtuell deklarieren soll. Das ist eigentlich kein Tip, sondern ein Muss. Denn ein schönes Design ist ja, eine Fabrikfunktion einzusetzen, die ein Objekt einer abgeleiteten Klasse zurückgibt. Dieses Objekt wird aber einem Basisklassenzeiger zugewiesen. Wird nun der Basisklassenzeiger benutzt, um das Objekt zu löschen, muss der Destruktor virtuell sein. Denn man will nicht, dass nur die Basisklassenanteile des Objekts zerstört werden.

Tip 9, während der Kontruktion der der Zerstörung keine virtuellen Funktionen aufzurufen, ist logisch und konnte ich an der Technik Fabrik-Methode extrahieren und überschreiben nachvollziehen.

Meyers Meinung nach muss Code, der mit Verweisen oder Zeigern auf mehrere Objekte desselben Typs arbeitet, in Betracht ziehen, dass die Objekte identisch sein können. Deshalb sollte man überprüfen, dass operator= sich bei der Zuweisung eines Objekes an sich selbst korrekt verhält.

Jeder Tip wie eine Tasse Wasser in der Wüste

Scott Meyers bescheidener Meinung nach ist sein Buch das 2. wichtigste C++-Buch nach Bjarne Stroustrups Die C++-Programmiersprache. Gleich danach kommt für ihn Design Patterns von Erich Gamma und Co, welches großen Einfluss auf den Gebrauch von C++ gehabt hatte.

Das am häufigsten gebrauchte Wort in seinem Ratgeber ist Kunde. Für Meyers sind die Kunden nicht nur die Programmierer, die den Code benutzen, sondern der Verfasser des Quelltextes selbst. Das überschneidet sich mit Robert C. Martins Feststellung in Clean Code,  dass man sich als Programmierer keineswegs ständig etwas Neues ausdenkt, sondern 9/10 des Tages damit verbringt, seinen eigenen Quelltext zu lesen. Denn natürlich besteht ja der Sinn im Programmieren, Dinge zu verfassen, die wieder verwendet werden.

Hier mal ein paar Beispiel-Tips:

Nach Möglichkeit const verwenden

In C++ gibt es vielfältige Möglichkeiten, const zu verwenden. Wenn man jetzt nicht nur noch darüber nachdenkt (mein Tip), was man alles als const deklarieren könnte, spricht nichts dagegen, es mehr und mehr einzusetzen. Und manche Programmierfehler werden dabei aufgedeckt. Defaultmäßige bitweise Konstanz von Klassenelementen kann nachträglich durch mutable in logische Konstanz aufgeweicht werden. Also nur Mut!

Objekte vor ihrer Verwendung initialisieren

Die Regeln, die definieren, wann ein Objekt initialisiert wird, sind sogar für Meyers zu kompliziert. Eine einfache Regel ist, einfach jedes Objekt vor Verwendung zu initialisieren. Nichtklassenelemente müssen manuell initialisiert werden. Bei Klassenelementen übernimmt diese Aufgabe der Konstruktor. Hier sollte allerdings die Elementinitialisierungsliste verfasst werden. Nur hier ist die Initialisierung ohne Ausnahme gewährleistet. Und man will sich ja über die Regeln keine Gedanken machen müssen. Zudem ist diese Initialisierung oft schneller als über den Umweg des Kopierzuweisungsoperators.
Da die Reihenfolge der Initialisierung nicht lokaler (außerhalb von Funktionen definierter) statischer Objekte in verschiedenen Übersetzungseinheiten (Übersetzungseinheit = Quelltextdatei + eingebundene Header) nicht definiert ist, ergeben sich Probleme wenn ein statisches Objekt ein anderes statisches Objekt benutzen will. Der Kniff besteht hier darin, eine Funktion zu schreiben, die die Referenz auf das statische Objekt zurück gibt. Dies ist eine einfache Anwendung des Singleton Pattern. Das globale statische Objekt wird durch Funktionskapselung lokal und für die Initialisierung von lokalen statischen Objekten übernimmt C++ vor deren Verwendung die Garantie.

Kompilierungsabhängigkeiten zwischen Dateien minimieren

C++ kann schlecht zwischen dem Klasseninterface und der Implementierung unterscheiden. Was in der Klasse Person als private deklariert ist, geht den Kunden nichts an, könnte man denken. Denn der benutzt ja nur die öffentlichen Funktionen. Ändern sich in der Deklaration aber private Daten oder Definitionen, muss alles neu kompiliert werden, was die geänderte Klasse benutzt. Ist die geänderte Klasse noch zentral, und man ist sich in seiner Implementierung noch nicht sicher, muss der Kunde ständig informiert werden, er möge doch erst mal alle Abhängigkeiten finden und gründlich neu kompilieren.

Die Lösung besteht darin, sich von Definitionen unabhängig zu machen und stattdessen von Deklarationen abzuhängen. Wo möglich sollten Klassen foreward deklariert werden. Wenn sich der Aufwand rechtfertigt, könnte man zwei Headerdateien für eine Übersetzungseinheit anlegen, eine mit der Deklaration und eine mit der Definition. Z.B. Bibliotheken, deren ‚Bücher‘ nun mal häufig wiederverwendet werden, sollte man so entwickeln.

Zwei weitere Möglichkeiten, die Abhängigkeit von Definitionen zu vermeiden, sind Handle-Klassen und Interfaces. Wenn die Klasse Person einen Zeiger auf ihre Implementierung (Pimple-Idiom) besitzt und diesen benutzt, um ihre öffentliche Schnittstelle zu implementieren, sind ihre Kunden nur von von ihrer Deklaration abhängig. Die Definition wurde in die Implementierungsklasse verlagert.

Dasselbe Ziel lässt sich mit einem Interface erreichen. Die Klasse Person mutiert zu einer abstrakten Basisklasse. In C++ kann diese im Gegensatz zu Java oder .NET eine Fabrikfunktion beinhalten, die eine konkrete Person erzeugt und einen Zeiger auf die abstrakte Person zurück liefert.