Aus Linux-Magazin 08/2014

Modernes C++ in der Praxis – Folge 17

Der C++-Compiler erzeugt einige Methoden automatisch. In C++11 kann der Programmierer das gezielt steuern, indem er eine Reihe deklarativer Schlüsselwörter verwendet.

Methoden anzufordern, zu unterdrücken oder zu überschreiben bereitet im klassischen C++ immer wieder Probleme. Der Programmierer muss dabei viele spezielle Regeln im Kopf behalten und anwenden, um keine böse Überraschung zu erleben. Das gehört nun der Vergangenheit an, denn mit modernem C++ lässt sich das Verhalten von Ableitungshierarchien rein deklarativ steuern.

Methoden anfordern und unterdrücken

Der Compiler erzeugt auf Bedarf einige Methoden selbst. Der Kasten “Automatisch” stellt die prominentesten vor. Die Vorteile dieser bei Notwendigkeit erzeugten Methoden liegen auf der Hand. Sie nehmen einerseits dem Profi die lästige Arbeit ab, immer wieder den gleichen Boilerplate-Code zu schreiben, und schützen andererseits den Anfänger davor, Fehler zu machen.

Automatisch

Bei Bedarf erzeugt der C++-Compiler folgende Methoden:

  • Default-Konstruktor
  • Copy/Move-Konstruktor
  • Copy/Move-Zuweisungsoperator
  • Operatoren »new« und »delete« in der einfachen Form
  • Operatoren »new« und »delete« für C-Arrays
  • Destruktor

Doch nicht immer generiert der C++-Compiler die richtige Methode oder entspricht die erzeugte Methode den Anforderungen des Programmierers. Jetzt schlägt die Stunde der neuen Schlüsselwörter »default« und »delete« . Während eine als »default« deklarierte Methode diese vom Compiler anfordert, unterdrückt »delete« eine Methodenerzeugung, die per Default zur Laufzeit verfügbar wäre. Listing 1 zeigt die rein deklarative Anwendung von »default« und »delete« .

Listing 1

default und delete

01 #include <utility>
02
03 class OnlyMove{
04   public:
05
06   OnlyMove()= default;
07
08   OnlyMove& operator= (const OnlyMove&)= delete;
09   OnlyMove (const OnlyMove&)= delete;
10
11   OnlyMove& operator= (OnlyMove&&)= default;
12   OnlyMove (OnlyMove&&) = default;
13 };
14
15 class OnlyOnStack {
16   public:
17     void* operator new(std::size_t)= delete;
18 };
19
20 class OnlyOnHeap{
21   public:
22     ~OnlyOnHeap()= delete;
23 };
24
25 void onlyDouble(double){}
26 template <typename T>
27 void onlyDouble(T)=delete;
28
29 int main(){
30
31   OnlyMove onlyMove;
32   OnlyMove onlyMove1(std::move(onlyMove));
33   // OnlyMove onlyMove2(onlyMove1);
34
35   OnlyOnStack onlyOnStack;
36   // OnlyOnStack* onlyOnStack1= new OnlyOnStack;
37
38   OnlyOnHeap* onlyOnHeap= new OnlyOnHeap;
39   // OnlyOnHeap onlyOnHeap1;
40
41   onlyDouble(3.14);
42   // onlyDouble(2011);
43
44 }

Das Listing stellt ein paar bekannte C++-Idiome vor, die sich mit C++11 sehr elegant lösen lassen, denn der Programmierer drückt darin einfach seine Intention aus. Der Compiler sorgt anschließend für die Implementierung. So sind Objekte der Klasse »OnlyMove« in den Zeilen 3 bis 13 ausschließlich verschiebbar, denn der Code unterbindet das Generieren des Copy-Konstruktors und -Zuweisungsoperators (Zeilen 8 und 9). Dagegen fordert er das Generieren des Move-Konstruktors und -Zuweisungsoperators (Zeilen 11 und 12) explizit an.

Auf Heap und Stack

Unterbindet der Programmierer wie in Zeile 17 die Generierung des »operator new« , so lässt sich das Objekt nicht auf dem Heap erzeugen, sondern nur noch auf dem Stack. Genau das Gegenteil bewirkt das Unterdrücken das Destruktors in Zeile 22. Objekte vom Typ »OnlyOnHeap« müssen alloziert werden.

Der Einsatz der neuen Schlüsselwörter beschränkt sich aber nicht auf Klassenhierarchien. Die freie Funktion »void onlyDouble(double)« (Zeile 25) stellt einen sehr interessanten Anwendungsfall in Kombination mit dem Funktionstemplate »template <typename T> void onlyDouble(T)= delete« (Zeilen 26 und 27) dar. Diese Kombination bewirkt, dass der Compiler für Argumente vom Typ »double« die Funktion, für alle anderen Argumente das Funktionstemplate verwendet. Die C++-Regel ist es, dass der Compiler im Zweifelsfall die freie Funktion vorzieht, falls die Argumente für diese gleich gut oder besser passen als für das Funktionstemplate.

In der Praxis

Zweimaliges Übersetzen des Programms bringt die ganze Magie ans Licht. Der erste Durchlauf (Abbildung 1) verwendet die Objekte entsprechend ihrer Definition, »onlyDouble()« erhält ein »double« -Argument. Der zweite Durchlauf (Abbildung 2) erfolgt, nachdem die auskommentierten Zeilen einkommentiert sind. Der Compiler quittiert dies mit eindeutigen Fehlermeldungen.

Abbildung 1: Übersetzen von <a href="#article_l1" class="listing" title=

Listing 1 mit den neuen Schlüsselwörtern.” width=”300″ height=”50″ /> Abbildung 1: Übersetzen von Listing 1 mit den neuen Schlüsselwörtern.

Abbildung 2: Der Compiler weist auf die Fälle hin, in denen das Programm gegen die Deklarationen mit »default« und »delete« verstößt.

Abbildung 2: Der Compiler weist auf die Fälle hin, in denen das Programm gegen die Deklarationen mit »default« und »delete« verstößt.

Die Copy-Konstruktion von »onlyMove2 (onlyMove1)« (Zeile 33), das Anlegen von »onlyOnStack1« auf dem Heap (Zeile 36), das Anlegen von »onlyOnHeap« auf dem Stack (Zeile 39) und das Aufrufen der Funktionsfamilie »onlyDouble()« mit der natürliche Zahl 2011 (Zeile 42) sind nicht zulässig. Der Compiler wählt für die natürliche Zahl das Funktionstemplate, das aber als »delete« erklärt ist.

Der Compiler erzeugt seine speziellen Methoden nach den folgenden Regeln:

  • Sie sind »public« und nicht virtuell.
  • Sie dürfen nicht als »explicit« deklariert sein und keine Ausnahmespezifikation besitzen.
  • Copy-Konstruktor und -Zuweisungsoperator erwarten konstante Lvalue-Referenzen.
  • Move-Konstruktor und -Zuweisungsoperator erwarten nicht-konstante Rvalue-Referenzen.

Im Artikel “Rasch verschoben” [1] lassen sich die Details zu Lvalue- und Rvalue-Referenzen nachlesen.

Sollen die automatisch erzeugten Methoden von dieser Standardform abweichen, muss der Programmierer sie außerhalb der Klasse definieren. Listing 2 zeigt typische Anwendungsfälle. Die Klasse »MyData« besitzt einen privaten Default-Konstruktor (1). Ihr Destruktor ist virtuell und hat eine Ausnahmespezifikation (2, 4). Der Copy-Konstruktor (3) ist explizit, der Copy-Zuweisungsoperator (5) erwartet eine nicht-konstante Lvalue-Referenz.

Listing 2

Abweichen von der Standardform

01 class MyData{
02 public:
03   explicit MyData(const MyData&);               // 3
04   MyData& operator= (MyData&);                  // 5
05   virtual ~MyData() throw();                    // 2, 4
06 private:
07   MyData();                                      // 1
08 };
09
10 MyData::MyData()= default;                       // 1
11 MyData::~MyData() throw()= default;              // 2, 4
12 MyData::MyData (const MyData&)= default;         // 3
13 MyData& MyData::operator= (MyData&)= default;    // 5

Vertrag mit dem Compiler

Die Schlüsselwörter »default« und »delete« beschreiben einen Vertrag zwischen Programmierer und Compiler. Der Programmierer deklariert seine Methoden, der Compiler sorgt im Falle von »default« für deren Implementierung oder schränkt bei »delete« die Auswahl von Methoden ein. Hier endet die Verbindlichkeit des Compilers aber noch nicht: Durch die neuen Schlüsselwörter »override« und »final« gewährleistet er, dass das Programm Methoden korrekt überschreibt oder das Überschreiben unterbindet.

Das Schlüsselwort »override« löst typische und schwer zu findende Probleme in komplexen Ableitungshierarchien. Um eine Methode richtig zu überschreiben, muss deren Signatur exakt stimmen. Was einfach klingt, gestaltet sich in der Praxis tückisch: Passt die Methode nicht exakt, verhält sich das Programm zwar syntaktisch richtig, doch die Semantik stimmt nicht, denn statt der gewünschten Methode wird eine andere Methode aufgerufen.

»override« ist Trumpf

Hier naht Rettung in Form des neuen Schlüsselworts »override« . Ist eine Methode damit deklariert, muss der Programmierer mit ihr eine Methode einer Basisklasse überschreiben. Ist dies nicht der Fall, moniert dies der Compiler unmissverständlich (Abbildung 3).

Abbildung 3: Beim Schlüsselwort »override« prüft der Compiler peinlich genau, ob der Programmierer wirklich die gewünschten Methoden überschreibt.

Abbildung 3: Beim Schlüsselwort »override« prüft der Compiler peinlich genau, ob der Programmierer wirklich die gewünschten Methoden überschreibt.

In Listing 3 läuft etwas schief! Die einfache Klasse »Base« (Zeilen 1 bis 10) besitzt fünf Methoden. Leider sind dem Autor beim Überschreiben der Methoden einige Fehler unterlaufen: »func1()« (Zeile 14) überschreibt keine Methode ihrer Basisklasse, da diese (Zeile 3) nicht virtuell und damit nicht zum Überschreiben vorgesehen ist. »func2()« (Zeile 16) besitzt das falsche Argument »double« , »func3()« (Zeile 3) ist nicht konstant und »func4()« (Zeile 20) gibt den Typ »int« zurück.

Listing 3

Überschreiben mit override

01 class Base {
02
03   void func1();
04   virtual void func2(float);
05   virtual void func3() const;
06   virtual long func4(int);
07
08   virtual void f();
09
10 };
11
12 class Derived: public Base {
13
14   virtual void func1() override;
15
16   virtual void func2(double) override;
17
18   virtual void func3() override;
19
20   virtual int func4(int) override;
21
22   virtual void f() override;
23
24 };
25
26 int main(){
27
28   Base base;
29   Derived derived;
30
31 }

Der letzte Versuch mit der Methode »f()« (Zeile 22) ist endlich erfolgreich, was sich in Abbildung 3 durch das Fehlen einer Fehlermeldung zeigt. Die Methoden der Klasse »Derived« müssen natürlich in klassischer C++-Syntax nicht als »virtual« deklariert werden, um virtuell zu sein. Da die Methoden der Klasse »Base« bereits virtuell sind, vererbt sie diese Eigenschaft an Methoden abgeleiteter Klassen. Das zusätzliche Schlüsselwort »virtual« hat sich aber als C++-Coding-Style etabliert [2].

Das Schlüsselwort »override« ist Kontext-sensitiv, stellt also nur in seinem besonderen Kontext ein Schlüsselwort dar. Diesen neuen Keyword-Typ hat C++11 erhalten, um bestehenden C++-Code nicht zu gefährden. Wäre »override« nicht Kontext-sensitiv, würden alle Programme syntaktisch ungültig, die den Namen »override« für Variablen, Funktionen oder auch Methoden verwenden.

Das letzte Wort

Neben »override« führt C++11 noch ein weiteres Kontext-sensitives Schlüsselwort ein: »final« . Mit dieser Deklaration kann der Programmierer zwei Anwendungsfälle explizit auf den Punkt bringen: Eine Methode, die sich nicht überschreiben lässt, und eine Basisklasse, von der er nicht ableiten darf. Beide Fälle stellt Listing 4 dar.

Listing 4

final-Methode und -Basisklasse

01 class Base {
02   virtual void h(int) final;
03 };
04
05 class Derived: public Base {
06   virtual void h(int);
07   virtual void h(double);
08   virtual void g(long) final;
09 };
10
11 struct FinalClass final { };
12 struct DerivedClass: FinalClass { };
13
14 int main(){
15
16   Base base;
17   Derived derived;
18
19   FinalClass finalClass;
20   DerivedClass derivedClass;
21
22 };

Wie geht nun der Compiler damit um, dass der Entwickler die Methode »h(int)« (Zeile 2) und die Struktur »FinalClass« (Zeile 11) als »final« deklariert hat? In Abbildung 4 ist zu erkennen, was beim Übersetzen des Codes geschieht. Wenig überraschend quittiert der Compiler das Kompilieren der Methode »h(int)« mit einer Fehlermeldung. Gleiches gilt für das versuchte Ableiten der Klasse, die als »final« erklärt ist.

Abbildung 4: Die als »final« erklärte Methode lässt sich nicht überschreiben und von der Klasse kann man nicht ableiten.

Abbildung 4: Die als »final« erklärte Methode lässt sich nicht überschreiben und von der Klasse kann man nicht ableiten.

Zwei Punkte sind an dem Beispielcode besonders interessant. Zum einen lässt sich eine Methode »h(double)« (Zeile 7) in der Klasse »Derived« definieren, die nicht genau die gleiche Signatur wie die Funktion »h(int)« (Zeile 2) besitzt. Zum anderen lässt sich die Klasse »Derived« um eine neue »final« -Methode namens »g(long)« erweitern. Das demonstriert, dass sich Methoden auch später in der Ableitungshierarchie noch als »final« deklarieren lassen.

Wie geht’s weiter?

Mit »default« und »delete« oder »override« und »final« endet bei Weitem noch nicht die verbesserte Unterstützung für objektorientiertes Programmieren in C++. Das Initialisieren von Objekten wird in der modernisierten Sprachversion deutlich mächtiger. So bietet C++11 Initialisiererlisten für Konstruktoren und das direkte Initialisieren von Klassenelementen an. Darüber hinaus unterstützt der neue Sprachstandard die Delegation und die Vererbung von Konstruktoren. Die Details samt Anwendungsbeispielen folgen wie immer in der nächsten Ausgabe dieser Artikelserie. (mhu)

Infos

  1. Rainer Grimm, “Rasch verschoben”: Linux-Magazin 02/12, S. 90
  2. Motor Industry Software Reliability Association (MISRA), “Guidelines for the use of the C++ language in critical systems”: http://www.misra-cpp.com

Der Autor

Rainer Grimm arbeitet als Software-Architekt und Gruppenleiter bei der Metrax GmbH in Rottweil. Insbesondere die Software der hauseigenen Defibrillatoren ist ihm eine Herzensangelegenheit. Seine Bücher “C++11 für Programmierer” und “C++ kurz & gut” sind beim Verlag O’Reilly erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben