Wilkening-Online Logo

Virtuelle Funktionen sollten in C++ niemals public sein



Von Detlef Wilkening
29.01.2012
Version: 1

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, z.B. auch mein C++ Tutorial[1] 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"; }
};

Aber wir wollen hier nicht über C++ für Anfänger reden, sondern über C++ für echte Programmierer. Über C++ Code der wartungsfreundlich und leicht erweiterbar ist - und da machen uns virtuelle public Funktionen schnell mal einen Strich durch die Rechnung.

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: Schauen wir uns mal beide Lösungen im Detail an:

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() {}
};

Die Implementierer von abgeleiteten Klassen haben nichts zu tun.

class B : public A
{
public:
   virtual void f() const { ... }       // Keine Aenderungen
};

Aber die Nutzer von "f" müssen jeden Aufruf von "f" anpassen.

void fct(const A& a)
{
   a.f_before();                        // Neu - vor jedem Aufruf von A::f notwendig
   a.f();
}

Wie Sie sicher sofort gesehen haben, ist dies nicht ernsthaft eine Lösung. Man kann nicht jeden Nutzer von "f" dazu verdonnern, vor "f" ein "f_before" aufzurufen. Das klappt einfach nicht: man erreicht nicht alle Nutzer, nicht alle haben Lust und Zeit das zu machen, usw. Es ist so einfach nicht praktikabel.

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
};

Die Implementierer von abgeleiteten Klassen müssen jetzt "f_before" aufrufen.

class B : public A
{
public:
   virtual void f() const
   {
      f_before();                       // Der Aufruf von 'f_before' ist neu
      ...
   }
};

Dafür haben diesmal die Nutzer von "f" nichts zu tun.

void fct(const A& a)
{
   a.f();                               // Keine Aenderungen
}

Das ist sicherlich schon eine bessere Lösung, denn diesmal muß nicht jeder Nutzer Änderungen vornehmen, sondern nur noch die Implementierer von abgeleiteten Klassen. Aber auch hier haben wir das Schwierigkeit, daß viele Implementierer etwas machen müssen, und wir keine Kontrolle darüber haben, ob sie es machen oder nicht. Und vielleicht wissen wir noch nicht mal, wie wir ihnen das überhaupt mitteilen können.

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
      ...
   }
};

Sie sehen - die Sache ist gar nicht so einfach für manchen Implementierer.

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
};

Der Implementierer muß nun die protected Funktion "f_work" überschreiben - aber das ist kein wirklicher Unterschied zu vorher.

class B : public A
{
protected:
   virtual void f_work() const;           // Die protected Funktion wird ueberschrieben
};

Der normale Nutzer merkt von all dem nichts, und hat weiter die public Funktion "f" zur Verfügung.

void fct(const A& a)
{
   a.f();                                 // Hier hat sich nichts geaendert
}

Abgesehen von der Indirektion "public f" auf "protected f_work" hat sich nichts wirklich geändert:

2.1 Aber nun können wir eingreifen

Muß 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
};

Für sowohl den Implementierer von abgeleiteten Klassen als auch den Nutzer hat sich nichts geändert - wir können jetzt ohne Probleme eingreifen. Und das geht nur, da wir quasi eine Schicht zwischen den Nutzer von außen und die Implementierer innen eingezogen haben. Hier können wir uns als Basis-Klassen-Implementierer austoben.

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() {}
};

Alle Implementierer von Kommandos - und davon gibt es in Programmen dann meist ziemlich viele - leiten sich nun von "command" ab und überschreiben "execute". Ich habe dieses Muster schon 1000 Mal in realen Projekten gesehen. Und 990 Mal habe ich erlebt, dass irgendwann die Anforderung kam, doch bitte: Und dann haben Sie gewonnen oder verloren - je nachdem wie Sie das betrachten. Ist Ihre virtuelle Funktion public, dann sollten Sie dieses Idiom nachziehen. Ansonsten müßten Sie in jeder Kommando-Klasse loggen, messen, abfangen oder sonstwas - und das kann ja wohl keine Lösung sein.

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 { ... }
}

Nun ist es häufig so, dass die Daten zum Abspeichern bestimmten Konsistenz-Bedingungen genügen müssen - z.B. müssen manche Daten-Felder zwingend ausgefüllt werden. Wenn dies nicht der Fall ist, dann darf das Speichern nicht passieren. Also muß vor dem Speichern ein entsprechender Check ausgeführt werden - der möglicherweise mit einem Abbruch der Aktion endet. Nun kann das natürlich jeder Programmierer immer in "store" machen, viel verständlicher ist aber eine "before_store" Funktion, die automatisch von der Basis-Klasse aufgerufen wird.

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 { ... }
}

Außerdem stellt die Basis-Klasse noch eine Default-Implementierung von "before_store" zur Verfügung, die aber nichts macht. Damit muß diese Funktion nur dann überschrieben werden, wenn ein entsprechender Bedarf besteht.

Ä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 { }
};

Damit bieten Sie automatisch eine ganze Möglichkeiten für den Implementierer an, ohne dass es wirklich was kostet. Die einzigen Nachteile sind: Dafür kann der Implementierer von abgeleiteten Klassen jetzt aus dem vollen Schöpfen, und das ist den Aufwand wert.

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 muß einen virtuellen Destruktor haben, und damit man dynamisch erzeugte Objekte mit "delete" wieder freigeben kann, muß 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

  1. Auch mein C++ Tutorial führt Polymorphie natürlich mit public virtual Funktionen ein

  2. Artikel "Inline in C++" von Detlef Wilkening
    • Noch nicht freigegeben

  3. 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

  4. Common Lisp Object System (CLOS):

  5. Das Web-Framework "Ruby on Rails":

  6. 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

  7. 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:

8. Versions-Historie

Die Versions-Historie dieses Artikels:
Schlagwörter: