09.04.2012
Version: 1
Inhaltsverzeichnis
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
}
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
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
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
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
- Falscher Name
- Falsche Parameterliste
- Falsches "const"
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
};
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
};
class A
{
public:
~A();
};
class B : public A
{
public:
virtual ~B() override; // Compiler-Fehler, kein virtueller Basisklassen-Destruktor
};
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
};
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
};
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;
};
// 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
};
- Wenn man Code schreibt, dann testet man ihn meist auch ausführlich - und das fehlerhafte Überschreiben fällt hoffentlich auf. Mit "override" fällt es nun schon früher und einfacher zur Compile-Zeit auf.
- Aber häufiger wird Code geändert - und dabei werden oft nicht alle Seiteneffekte bedacht und/oder getestet. Diese Probleme fallen jetzt zur Compile-Zeit auf - "override" sei Dank.
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
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:
- GCC 4.7[8]
-
Microsoft Visual Studio 2010 (eingeschränkt) und 2011 Beta[9] (vollständig)
- Obwohl ich nirgends eine offizielle Aussage dazu gefunden habe, unterstützt das Microsoft Visual Studio 2010 "override" schon eingeschränkt. Abgesehen von der Nutzung beim Destruktor funktionieren alle anderen Beispiele dieses Artikels auch mit diesem Compiler.
- Einen besonderen Dank an Klaus Wittlich, der die Beispiele einfach mal mit dem MSVS2010 ausprobiert und mich dann auf diesen Umstand hingewiesen hat.
- Clang 3.0[10]
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
-
Artikel "Überladung und Verdeckung von Symbolen in C++" von Detlef Wilkening
- Noch nicht freigegeben
-
Artikel "Überladen mit Const in C++" von Detlef Wilkening
- Noch nicht freigegeben
-
Artikel "Kovariante Rückgabe-Typen bei virtuellen Funktionen in C++" von Detlef Wilkening
- Noch nicht freigegeben
-
Artikel "Was erzählt in C++ der Destruktor einer Klasse über die Klasse?" von Detlef Wilkening
- Noch nicht freigegeben
-
Artikel "C++11 Feature final" von Detlef Wilkening
- Noch nicht freigegeben
-
C++11 Standard
§ 2.11, Item 2 und Tabelle 3
ISO/IEC 14882, Third edition, 2011-09-01 -
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 - GCC 4.7
- Microsoft Visual Studio 2011 Beta
- 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:- Alexander Heckner
- Klaus Wittlich
- Ralph Habermann
- Robert Wittek
- Sven Johannsen
9. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 09.04.2012
- Initiale Version