Wilkening-Online Logo

C++11 Feature "override"



Von Detlef Wilkening
09.04.2012
Version: 1

1. Das Problem

Ein typisches Problem beim Programmieren ist Vertippen - Sie meinen "size" und tippen "seze". Das passiert jedem ab und zu und meistens findet der Compiler den Fehler. Die Variable "seze" gibt es nicht - und damit meckert der Compiler.

#include <iostream>
using namespace std;

int main()
{
   int size = 42;
   cout << seze << endl;   // Compiler-Fehler - Symbol "seze" ist unbekannt
}

Leider gibt es auch Vertipper, die der Compiler nicht findet, und die sich auch nicht so schnell bemerkbar machen. In allen objekt-orientierten Sprachen gehört dazu das fehlerhafte Überschreiben von Funktionen:

class A
{
public:
   virtual void fct(int);
};

class B : public A
{
public:
   virtual void fcd(int);       // Anderer Name
};                              // => kein Ueberschreiben, aber auch kein Compiler-Fehler

Gefährlich ist das fehlerhafte Überschreiben, da nicht nur der Name der Funktion eingeht, sondern auch die Parameterliste. Wird eine andere Parameterliste angegeben, so ist dies kein Überschreiben mehr, sondern eine Art des Überladens - hier noch zusätzlich mit Verdeckung des Orginal-Symbols[1].

class A
{
public:
   virtual void fct(int);
};

class B : public A
{
public:
   virtual void fct(long);      // Gleicher Name, anderer Parameter-Typ
};                              // => kein Ueberschreiben, aber auch kein Compiler-Fehler

Das ist vielleicht kein Vertipper mehr, sondern ein Programmierer-Irrtum - ändert aber nichts daran, dass das nicht der gewünschte Code ist und der Compiler keinen Fehler meldet.

Noch netter ist dieser Fehler, wenn sich die beiden Funktionen nur durch "const" unterscheiden. Es gibt leider immer noch zuviele C++ Entwickler, die nicht wissen, dass man Element-Funktionen mit "const" überladen kann - aber es ist möglich und wird auch gemacht[2].

class A
{
public:
   virtual void f(int) const;
   virtual void g(int);          // Kein "const"
};

class B : public A
{
public:
   virtual void f(int);           // Gleicher Name, gleiche Parameter, kein "const"
   virtual void g(int) const;     // Gleicher Name, gleiche Parameter, aber "const"
};                                // => kein Ueberschreiben, aber auch kein Compiler-Fehler

Ein fehlerhafter Rückgabe-Typ (ohne Kovarianz[3]) ist dagegen kein Problem, da dies in C++ ein Fehler ist und schon immer war:

class A
{
public:
   virtual void f(int);
};

class B : public A
{
public:
   virtual int f(int);            // Compiler-Fehler
};                                // Rueckgabe-Typ muss gleich oder kovariant sein

Unterm Strich sehen wir aber, dass es einige Situationen gibt, bei denen der Programmierer sich vertippt oder vertut - und schon hat der Quelltext eine andere Semantik und ist fehlerhaft:

2. Retter C++11

2.2 Funktionen

In C++11 kann man nun eine überschreibende Funktion mit "override" kennzeichnen - und überschreibt man sie nicht wirklich, dann bekommt man einen Compiler-Fehler.

class A
{
public:
   virtual void fct(int);

           void fct0(int);
   virtual void fct1(int);
   virtual void fct2(int);
   virtual void fct3(int) const;
   virtual void fct4(int);
};

class B : public A
{
public:
   virtual void fct(int) override;          // Korrektes Ueberschreiben - alles okay

   virtual void fct0(int) override;         // A-Funktion nicht virtuell => Compiler-Fehler
   virtual void fct1x(int) override;        // Falscher Name => Compiler-Fehler
   virtual void fct2(long) override;        // Falscher Parameter => Compiler-Fehler
   virtual void fct3(int) override;         // Falsches Const => Compiler-Fehler
   virtual void fct4(int) const override;   // Falsches Const => Compiler-Fehler
};

Die Markierung einer virtuellen Funktion mit "override" drückt aus, dass man eine Funktion der Basis-Klasse überschreiben will. Gibt es in der Basis-Klasse diese Funktion nicht oder ist sie nicht virtuell, so bekommt man einen Compiler-Fehler. Mit "override" fallen daher kleine Flüchtigkeits-Fehler (und manchmal auch große Design-Fehler) schon zur Compile-Zeit auf.

2.2 Destruktoren & Konstruktoren

Override funktioniert für alle virtuellen Element-Funktionen, also auch für Destruktoren. Destruktoren sind Element-Funktionen, nur eben spezielle - und sie können virtuell sein, müssen es in vielen Fällen ja sogar sein[4].

Beispiel 1 - korrektes Überschreiben mit virtuellen Destruktoren:

class A
{
public:
   virtual ~A();
};

class B : public A
{
public:
   virtual ~B() override;   // Okay
};

Beispiel 2 - fehlerhaftes Überschreiben, wenn der Basisklassen-Destruktor nicht virtuell ist:

class A
{
public:
   ~A();
};

class B : public A
{
public:
   virtual ~B() override;   // Compiler-Fehler, kein virtueller Basisklassen-Destruktor
};

Beispiel 3 - fehlerhaftes Überschreiben, da der implizite Destruktor nicht virtuell ist:

class A
{
   // Keine Destruktor-Deklaration, daher nur der impliziter Destruktor
};

class B : public A
{
public:
   virtual ~B() override;   // Compiler-Fehler, der implizite Destruktor ist nicht virtuell
};

Konstruktoren können in C++ nicht virtuell sein, daher kann man sie nicht überschreiben, daher macht hier die Verwendung von "override" keinen Sinn und ist immer ein Compiler-Fehler.

class A
{
public:
   A();
};

class B : public A
{
public:
   B() override;          // Compiler-Fehler - Konstruktoren sind nie virtuell
};

2.3 Operatoren

Und auch Operatoren können virtuelle Element-Funktionen sein, daher kann man hier "override" auch einsetzen:

class A
{
public:
   virtual bool operator==(const A&) const;
};

class B : public A
{
public:
   virtual bool operator==(const A&) const override;       // Okay
};

Und bei fehlerhaftem Überschreiben hilft auch hier wieder "override":

class A
{
public:
   virtual bool operator==(const A&) const;
};

class B : public A
{
public:
   virtual bool operator==(const B&) const override;       // Compiler-Fehler - B-Parameter
};

3. Aber vor allem schützt uns "override" bei Änderungen

Das Einführungs-Szenario ist schon realistisch - man irrt sich bei einem Parameter oder vergißt das "const" - und schon hat man nicht überschrieben. Override hilft einem hier - der Fehler fällt direkt auf.

Die wirkliche Hilfe von "override" fällt aber viel später auf. Jemand ändert in der Basis-Klasse die Funktions-Deklaration (fügt z.B. einen Parameter mit Default-Wert hinzu) und vergißt diese Schnittstellen-Änderung in den abgeleiteten Klassen nachzuziehen. Bislang fiel das nicht auf - mit "override" jetzt schon.

Nehmen wir an, dies ist der alte funktionierende(!) Code:

// Vorhandener Code - alles ist okay

class A
{
public:
   virtual void fct(int) const;
};

class B : public A
{
public:
   virtual void fct(int) const override;
};

Nun wird der Code verändert - in die Basisklassen-Funktion wird ein weiterer Parameter eingeführt. Und der Programmierer denkt mit: er führt den Parameter mit Default-Argument ein, damit die Aufruf-Stellen nicht angepaßt werden müssen. Doch an die überschreibenden Funktionen denkt er nicht. Aber "override" paßt auf.

// Geaenderter Code - jetzt hilft "override"

class A
{
public:
   virtual void fct(int, int=42) const;
};

class B : public A
{
public:
   virtual void fct(int) const override;        // Compiler-Error - "override" sei Dank
};

In der Praxis ist das die wirkliche Hilfe:

4. Bezeichner "override" - kein Schlüsselwort

Aber Achtung - "override" ist kein Schlüsselwort. Es ist ein fast normaler Bezeichner. Nur hat "override" (wie auch der C++11 Bezeichner "final"[5] in bestimmten Kontexten eine besondere Bedeutung[6]. Hinter der Deklaration einer virtuellen Funktion ist "override" kein Symbol des Programmierers, sondern der Sprache - und weist den Compiler an zu überprüfen, dass diese Funktion eine Überschreibende ist.

Hintergrund dieser etwas komischen Regel ist natürlich, dass C++11 keinen alten C++ Code brechen wollte, bei dem der Programmierer "override" z.B. als Variablen-Namen eingesetzt hat. Dies ist nämlich weiterhin möglich - denn als Variable befindet sich der Bezeichner in einem anderen Kontext und wird als normaler Variablen-Name interpretiert.

#include <iostream>
using namespace std;

int main()
{
   int override = 42;             // Kein Problem - "override" ist ein normaler Bezeichner
   cout >> override >> endl;      // Kein Problem - "override" ist ein normaler Bezeichner
}


Ausgabe:

42

Da dies aber Einsteiger in C++ verwirren könnte - halte ich die Nutzung von "override" und auch "final" z.B. als Variablen- oder Funktions-Bezeicher für nicht empfehlenswert.

Dies ist übrigens ein netter Test für das Syntax-Highlighting Ihres Editors. Wenn es wirklich gut ist, dann ist "override" fast immer nur ein normaler Bezeichner - wird aber in dem speziellen Kontext anders dargestellt, um die besondere Bedeutung zu betonen. Aber kann das ein Editor?

Eine weitere Konsequenz dieser Regel betrifft den Präprozessor: da der Kontext Teil der Sprache C++ ist, und die Sprach-Regeln vom Präprozessor nicht beachtet werden, würde ein Präprozessor-Makro "override" auch das "override" in diesen speziellen Kontexten ersetzen und damit einen Compiler-Fehler erzeugen. Der neue C++11 Standard verbietet daher u.a. explizit den Bezeichner "override" als Präprozessor-Makro-Namen[7].

5. Praxis

Alles schön, sehr schön - aber das ist C++11. Kann man das auch praktisch nutzen? Gibt es denn schon Compiler, die "override" unterstützen?

Ja - mir sind aktuell (09.04.2012) mehrere Compiler bekannt die "override" unterstützen, und ich habe sicher nicht den Gesamtüberblick: Wer das Glück hat, mit diesen modernen Compilern arbeiten und Code schreiben zu dürfen, der nicht abwärts-kompatibel sein muss - der kann "override" schon nutzen. Alle anderen müssen noch warten.

6. Fazit

Override ist nur eine winzige Kleinigkeit unter den vielen vielen Neuigkeiten im C++11 Standard. Aber "override" hilft, unnötige Fehler zu vermeiden bzw. sie schon früh und einfach zu erkennen - und damit hat "override" seine Berechtigung. In Java hat man einen vergleichbaren Check mit der Annotation "@override" schon seit Java 5 - und der Check hat sich dort bewährt. C++ kann das jetzt auch.

7. Links

  1. Artikel "Überladung und Verdeckung von Symbolen in C++" von Detlef Wilkening
    • Noch nicht freigegeben

  2. Artikel "Überladen mit Const in C++" von Detlef Wilkening
    • Noch nicht freigegeben

  3. Artikel "Kovariante Rückgabe-Typen bei virtuellen Funktionen in C++" von Detlef Wilkening
    • Noch nicht freigegeben

  4. Artikel "Was erzählt in C++ der Destruktor einer Klasse über die Klasse?" von Detlef Wilkening
    • Noch nicht freigegeben

  5. Artikel "C++11 Feature final" von Detlef Wilkening
    • Noch nicht freigegeben

  6. C++11 Standard
    § 2.11, Item 2 und Tabelle 3
    ISO/IEC 14882, Third edition, 2011-09-01

  7. C++11 Standard
    C.2. 7, Clause 17, Item 17.6.5.3
    Zusätzliche Beschränkungen von Makro-Namen
    ISO/IEC 14882, Third edition, 2011-09-01

  8. GCC 4.7

  9. Microsoft Visual Studio 2011 Beta

  10. Clang 3.0

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

9. Versions-Historie

Die Versions-Historie dieses Artikels:
Schlagwörter: