29.01.2012
Version: 1
Inhaltsverzeichnis
1. Virtuelle Funktionen sollten niemals public sein
Ja wirklich - in C++ sollten virtuelle Funktionen niemals "public" sein. Okay, man kann es auch umdrehen: In C++ sollten public Funktionen niemals virtuell sein. Aber wie man es auch dreht und wendet: "public" und "virtual" passen einfach nicht zusammen.Und falls Sie auch wissen wollen warum, so lesen Sie weiter... Fangen wir ganz vorne an. Natürlich kann man in C++ auch public Funktionen virtuell machen, oder virtuelle Funktionen public - jedes C++ Lehrbuch macht dies so. Es ist einfach der ganz normale Einstieg in die Polymorphie.
class A
{
public:
virtual ~A() {}
virtual void f() const { cout << "A::f()\n"; }
protected:
A() {}
};
class B : public A
{
public:
virtual void f() const { cout << "B::f()\n"; }
};
class C : public A
{
public:
virtual void f() const { cout << "C::f()\n"; }
};
1.2 Die Story zum Idiom
Bleiben wir bei dem einfachen Beispiel mit der Basis-Klasse "A" und ihrer public virtuellen Funktion "f". Stellen Sie sich vor, Sie hätten diese Klasse geschrieben - natürlich gedacht als Basis-Klasse, von der Sie - aber auch Ihre Freunde und Kollegen, und letztlich jeder Nutzer der Klasse - natürlich ableiten soll, um dann u.a. "f" zu überschreiben. Die Wochen gehen ins Land, und mittlerweile haben viele Leute - auch welche, die Sie gar nicht kennen - ihre Klassen von der "A" abgeleitet und "f" überschrieben.Plötzlich stellen Sie fest, dass Ihre Basis-Klasse "A" ein fundamentales Problem hat, das im Prinzip ganz einfach zu lösen wäre: Es müßte nur vor der Ausführung der eigentlichen F-Funktionalität irgendeine Kleinigkeit gemacht werden. Jetzt haben Sie zwei Möglichkeiten:
- Sie bieten diese zusätzliche Kleinigkeit als zusätzliche public Funktion "f_before" an, und bitten alle Nutzer vor "f" immer zuerst "f_before" aufzurufen.
- Oder Sie implementieren "f_before" protected, rufen "f_before" direkt am Anfang von "f" auf, und bitten alle Implementierer von abgeleiteten Klassen am Anfang auch "f_before" aufzurufen - außer sie bekommen das schon durch einen Basis-Klassen-Funktions-Aufruf geschenkt.
1.2.1 Public Funktion "f_before" für den normalen Nutzer
Wir stellen in unserer Basis-Klasse die Problem-Lösung in Form einer zusätzlichen Public-Funktion "f_before" zur Verfügung.
class A
{
public:
virtual ~A() {}
void f_before() const { ... } // Neue Funktion mit Problem-Loesung
virtual void f() const {... } // Keine Aenderungen
protected:
A() {}
};
class B : public A
{
public:
virtual void f() const { ... } // Keine Aenderungen
};
void fct(const A& a)
{
a.f_before(); // Neu - vor jedem Aufruf von A::f notwendig
a.f();
}
1.2.2 Protected Funktion "f_before" für die Implementierer abgeleiteter Klassen
Machen wir es also anders - stellen wir die Problem-Lösung als Protected-Funktion "f_before" in der Basis-Klasse zur Verfügung, und setzen auf die Implementierer statt auf die Nutzer.
class A
{
public:
virtual ~A() {}
virtual void f() const { ... } // Keine Aenderungen
protected:
A() {}
void f_before() const { ... } // Neue Funktion mit Problem-Loesung
};
class B : public A
{
public:
virtual void f() const
{
f_before(); // Der Aufruf von 'f_before' ist neu
...
}
};
void fct(const A& a)
{
a.f(); // Keine Aenderungen
}
1.2.3 Manche Implementierer haben noch ein weiteres Problem
Genau genommen kann die Aufgabe für den Implementierer viel schwerer werden, als sie zuerst aussieht. Nehmen wir mal an, der Implementierer leitet sich gar nicht direkt von der Basis-Klasse "A" ab, sondern von einer Zwischen-Klasse "B", die auch "f" überschreibt. Und die Implementierung in "C" nutzt als erstes die geerbte Implementierung von "f". Was dann?
class B : public A // Die Klasse eines fremden Implementierers
{
public:
virtual void f() const; // Wie ist 'f' hier implementiert? Ist der Fix schon eingebaut?
};
class C : public B // Meine Klasse, nicht direkt von 'A' abgeleitet
{
public:
virtual void f() const
{
// Was macht man hier nun? Fix einbauen, oder nicht?
B::f(); // Aufruf der geerbten Funktion
...
}
};
Alles in allem - auch die zweite Lösung ist keine wirkliche Lösung, selbst wenn vielleicht etwas besser als die erste Lösung. Was macht man denn nun?
2. Machen Sie virtuelle Funktionen niemals public
Ganz einfach: Machen Sie virtuelle Funktionen niemals public!Fangen wir noch mal ganz von vorne an, und erinnern uns an das vorgeschlagene Idiom, selbst wenn es anfangs etwas komisch klang. Machen wir die virtuelle Funktion doch unter einem neuen Namen (hier "f_work") protected, und rufen sie dann aus der alten nun nicht-virtuellen inline public Funktion "f" auf.
class A
{
public:
virtual ~A() {}
inline void f() const { f_work(); } // Nach aussen ist alles beim alten
protected:
A() {}
virtual void f_work() const; // Die protected Funktion ist nun ueberschreibbar
};
class B : public A
{
protected:
virtual void f_work() const; // Die protected Funktion wird ueberschrieben
};
void fct(const A& a)
{
a.f(); // Hier hat sich nichts geaendert
}
- Die public Schnittstelle ist die gleiche geblieben.
- Abgeleitete Klassen können weiterhin die F-Funktionalität verändern - nur müssen sie jetzt "f_work" statt "f" überschreiben.
- Da die Funktion "f" inline ist, kann der Compiler die Funktion "f" komplett wegoptimieren, und die Performance unterscheidet sich nicht vom Original-Code [2].
2.1 Aber nun können wir eingreifen
muss nun vor jeder F-Funktionalität neuer Code ausgeführt werden, so können wir dies problemlos in "f" integrieren, und dies wirkt für unser "f_work" als auch für jede überschriebene Funktion "f_work".
class A
{
public:
virtual ~A() {}
inline void f() const // Nach aussen ist alles beim alten
{
f_before(); // Hier kann man ohne Probleme eingreifen
f_work();
}
protected:
A() {}
virtual void f_work() const;
private:
void f_before() const { ... } // Neue Funktion mit Problem-Loesung
};
2.2 Das betrifft auch pure-virtual Funktionen
Es sollte eigentlich klar sein, aber ich will es lieber noch mal explizit erwähnen. Das Idiom macht auch für pure-virtual Funktionen Sinn - also auch, wenn die Basis-Klasse keine Default-Implementierung für "f" bzw. "f_work" anbietet.
class A
{
public:
virtual ~A() {}
inline void f() const { f_work(); }
protected:
A() {}
virtual void f_work() const = 0; // Auch fuer pure-virtual Funktionen geht das
};
2.3 Aber ist das denn wirklich notwendig?
Ich spüre noch einige Zweifel und Fragen: "Ist das denn wirklich sinnvoll? Ich meine: wann hat man denn genau so einen Fehler, der auch alle überschriebenen Funktionen betrifft, und dann nur durch vorweggeschalteten Code beseitigt werden kann? Ist das nicht einfach nur ein konstruierter in der Praxis nicht relevanter Sonderfall?"Jein. Das Beispiel ist sicher konstruiert - so einen Fehler hat man sicher extrem selten. Aber andere Anforderungen, die dieses Idiom abdecken kann, sind alles andere als selten.
Nehmen wir z.B. an, die Basis-Klasse "A" ist in Wirklichkeit eine Basis-Klasse "command" für Kommandos, und die Funktion "f" ist die Funktion "execute" des Kommandos - also eine ganz typische Implementierung des Command-Patterns[3].
class command
{
public:
virtual ~command() {}
virtual void execute() = 0;
protected:
command() {}
};
- jede Kommando-Ausführung zu loggen, oder
- die Ausführungs-Dauer jedes Kommandos zu loggen oder in einen Benutzer-View zu schreiben, oder
- für alle Kommandos spezielle Exceptions abzufangen und speziell zu reagieren, oder
- oder, oder...
Seien Sie also vorbereitet, und machen Sie virtuelle Funktionen nie public, sondern arbeiten mit protected virtuellen Funktionen und einer public-inline Wrapper-Funktion.
class command
{
public:
virtual ~command() {}
inline void execute() { real_execute(); }
protected:
command() {}
virtual void real_execute() = 0;
};
3. Before- und After-Funktionen
Im Prinzip könnten wir jetzt mit diesem Idiom aufhören, aber es gibt da noch etwas, über das sich zu schreiben lohnt - sogenannte Before- und After-Funktionen. In der Praxis passiert es häufiger, dass man vor bzw. nach einer Funktion immer bestimmte Aktionen durchführen möchte - möglicherweise sogar mit dem Einfluß die eigentlich Funktion gar nicht mehr aufzurufen, wenn vorher ein bestimmter Status vorliegt. Schauen wir uns ein Beispiel dafür an:Wir haben ein typisches Daten-Verwaltungs-Programm, in dem es viele Klassen für die zu verwaltenen Daten gibt - u.a. die Klasse "data". Da alle Daten des Programms letztlich in einer Datenbank gespeichert werden, haben alle Daten-Klassen eine gemeinsame Basis-Klasse "data_base", die sich u.a. um das Datenbank-Geraffel kümmert. Diese Basis-Klasse hat dabei typischerweise auch eine Store-Funktion "store" zum Speichern der Daten in die Datenbank. Nun kann die Basis-Klasse nicht wissen, wie sie z.B. "data" zu speichern hat, d.h. ist "store" natürlich pure-virtual - und in "data" ist die Funktion "store" dann überschrieben.
class data_base
{
public:
virtual ~data_base() {}
virtual void store() const = 0;
protected:
data_base() {}
};
class data : public data_base
{
public:
virtual void store() const { ... }
}
class data_base
{
public:
virtual ~data_base() {}
inline void store() const
{
if (before_store())
{
process_store();
}
}
protected:
data_base() {}
virtual bool before_store() const { return true; }
virtual void process_store() const = 0;
};
class data : public data_base
{
protected:
virtual bool before_store() const { ... }
virtual void store() const { ... }
}
Ähnliche Aufgaben kann man sich für den Zeitpunkt nach dem Speichern vorstellen - z.B. einen Benutzer-Hinweis, Zeitmessungen, Logs, usw.
Letzlich läuft das Idiom und dieser Abschnitt darauf hinaus, uns klar zu machen, dass es häufig sinnvoll ist, sich vor und hinter einen Funktions-Aufruf einklinken zu können. Das ist so "interessant", dass es sogar Sprachen gibt die Before- und After-Funktionen als Feature anbieten, z.B. CLOS[4]. Und auch viele Frameworks stellen entsprechende Funktionen zur Verfügung, wie z.B. das Web-Framework "Ruby on Rails"[5]. beim Speichern von Daten mit Active-Records.
Okay, das ist ja alles ganz interessant - aber was hat das mit C++ und diesem Idiom zu tun?
3.1 Stellen Sie Before- und After-Funktionen zur Verfügung
Wenn Sie schon dabei sind, eine gute Basis-Klasse zu entwickeln, dann denken Sie an die späteren Nutzer und stellen prophylaktisch für alle (zumindest fast alle) virtuellen Funktionen auch Before- und After-Funktionen zur Verfügung.
class base
{
public:
virtual ~base() {}
inline void fct() const
{
if (before_fct())
{
process_fct();
after_fct();
}
}
protected:
base() {}
virtual bool before_fct() const { return true; }
virtual void process_fct() const = 0;
virtual void after_fct() const { }
};
- Sie müssen etwas mehr tippen - aber das sind halt die Kosten für gute Basis-Klassen.
- Und die Ausführung der Funktion ist minimal langsamer, da nun immer die Aufrufe und die leeren Before- und After-Default-Implementierungen zuschlagen. Aber wenn das Ihre Performance-Bottlenecks sind, dann haben Sie ganz andere Probleme.
4. Weiteres
4.1 Destruktoren
Im Prinzip müßte diese Regel auch für den Destruktor gelten. Scheinbar könnte es auch hier sinnvoll sein, vor oder nach seiner Ausführung noch Aktionen einhängen zu können. Aber hier schiebt die Sprache einen Riegel vor. Eine Basis-Klasse muss einen virtuellen Destruktor haben, und damit man dynamisch erzeugte Objekte mit "delete" wieder freigeben kann, muss der Destruktor "public" sein.Zum Glück ist der Destruktor mehr eine technische als eine fachliche Funktion. Er zerstört das Objekt und gibt dem Programmierer die Möglichkeit hierbei kontrolliert aufzuräumen. Ein Destruktor sollte keine Benutzer-Interaktion machen, sollte nicht verhindert werden können, und sollte nicht von anderen Bedingungen abhängen. Außerdem ist das Objekt nach dem Destruktor weg - eine After-Funktion macht einfach keinen Sinn.
Von daher bleibt der Destruktor die Ausnahme dieser Regel, mit der wir ganz gut leben können.
4.2 Interfaces
Hinweis - dieser Abschnitt ist für alle die, die neben C++ auch Java oder C# kennen und vielleicht sogar programmieren müssen. Alle anderen können ihn gerne überspringen.Wie Sie gesehen haben, kann man mit einfachen Mitteln eine Basis-Klasse sehr flexibel und vielseitig gestalten. Dies funktioniert u.a. deshalb immer problemlos, da C++ als "Basis-Typen" nur echte Klassen kennt. In Sprachen, deren Philosophie auf "Basis-Typen" ohne Implementierungen beruht, wie z.B. Java mit seinen Interfaces, ist man nicht so gut dran. In Java Interfaces sind alle virtuellen Funktionen sind automatisch public, und einfache Wrapper-Funktionen sind auch nicht machbar. Seien wir also froh, dass wir C++ programmieren dürfen [6].
4.3 Template-Method Pattern
Ein bisschen erinnert dieses Idiom mit den Before- und After-Methoden an das Template-Method Pattern[7]. Aber letztlich sind dieses Idiom und das Pattern doch unterschiedlich: beim Template Method Design Pattern geht es primär darum, den Algorithmus in kleine "modifizierbare" Stücke aufzubrechen. Dagegen greifen wir hier in keiner Weise in den Algorithmus ein, sondern ermöglichen nur Vor- und Nach-Bedingungen. So hat ja z.B. eine Zeit-Messung nichts mit dem eigentlichen Algorithmus zu tun. Und die Default Before und After-Funktionen sind ja auch leer, und beinhalten keine Algorithmen-Anteile. Trotzdem sind natürlich gewisse Ähnlichkeiten da - aber ich würde sie nicht überbewerten.5. Fazit
Halten wir fest: Machen Sie virtuelle Funktionen mit Ausnahme des Destruktors niemals public. Nutzen Sie statt dessen eine public Wrapper-Funktion, die im einfachsten Fall inline ist und nur die virtuelle Funktion direkt aufruft - und mehr nicht. Das ganze macht nicht viel Arbeit, und kostet weder Performance, noch Speicherplatz, noch stellt es für den Nutzer einen zusätzlichen Aufwand da.Und wenn Sie schon dabei sind, eine gute gern-genutzte Basis-Klasse zu entwickeln, oder gar eine ganze Bibliothek - dann denken Sie doch auch gleich über Before- und After-Funktionen nach. Ihre Nutzer werden es Ihnen danken.
6. Links & Literatur
-
Artikel "Inline in C++" von Detlef Wilkening
- Noch nicht freigegeben
-
Falls Sie das Command-Pattern (auch "Kommando") nicht kennen - hier ein paar Empfehlungen:
- http://de.wikipedia.org/wiki/Kommando_%28Entwurfsmuster%29
-
Buch "Entwurfsmuster, Elemente wiederverwendbarer objektorientierter Software"
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
- Sprache: Deutsch
- Verlag: Addison-Wesley
- ISBN: 978-3827330437
-
Buch "Entwurfsmuster von Kopf bis Fuß"
- Eric Freeman, Elisabeth Freeman, Kathy Sierra, Bert Bates
- Sprache: Deutsch
- Verlag: O'Reilly
- ISBN: 978-3897214217
- Common Lisp Object System (CLOS):
- Das Web-Framework "Ruby on Rails":
-
Wer mehr zum Thema "Interfaces" und ihre Vor- und Nachteile wissen will -
dem empfehle ich den Artikel "Mythos Interfaces" von Detlef Wilkening:
- Noch nicht freigegeben
-
Falls Sie das Template-Method-Pattern (auch "Schablonen-Methode") nicht kennen - hier ein paar Empfehlungen:
- http://de.wikipedia.org/wiki/Template_Method
-
Buch "Entwurfsmuster, Elemente wiederverwendbarer objektorientierter Software"
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
- Sprache: Deutsch
- Verlag: Addison-Wesley
- ISBN: 978-3827330437
-
Buch "Entwurfsmuster von Kopf bis Fuß"
- Eric Freeman, Elisabeth Freeman, Kathy Sierra, Bert Bates
- Sprache: Deutsch
- Verlag: O'Reilly
- ISBN: 978-3897214217
7. Danksagung
Bedanken möchte ich mich bei folgenden Personen (in alphabetischer Reihenfolge), die diesen Artikel gegengelesen und mit vielen kleinen und großen Hinweisen dafür gesorgt haben, dass er besser ist als am Anfang:- Alexander Heckner
- Klaus Wittlich
- Ralph Habermann
- Robert Wittek
- Sven Johannsen
8. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 29.01.2012
- Initiale Version