Aus Linux-Magazin 10/2014

Modernes C++ in der Praxis – Folge 18

Die Usability von C++ als objektorientierte Sprache lebt davon, wie einfach es ist, neue Objekte zu erzeugen. Kein Wunder, dass C++11 gegenüber früheren Versionen diesen Baugrund besser erschlossen hat.

In C++11 lassen sich zum einen Container mit Initialisiererlisten in einem Rutsch und Klassenelemente direkt initialisieren. Zum anderen kennt C++ die Delegation und Vererbung von Konstruktoren. Diesen Features ist gemein, dass sie keine große Neuerungen mit sich bringen. Zusammen erleichtern sie das Programmierleben aber ungemein.

Die Initialisierung eines Containers der früheren Standard Template Library war mit viel Tipparbeit verbunden. So musste der Programmierer jedes Element einzeln mit der Methode »push_back()« auf den Container schieben. Selbst das Initialisieren eines kleinen Vektors erfordert einige Schreibarbeit, wie das Beispiel

std::vector<int> myVec;
myVec.push_back(1);
myVec.push_back(2);
myVec.push_back(3);
myVec.push_back(4);
myVec.push_back(5);

zeigt. Um wie viel angenehmer ist dagegen die direkte Initialisierung eines Vektors in der modernen C++-Syntax:

std::vector<int> myVec={1,2,3,4,5};

Die Details zur modernen Syntax mit einer so genannten »{}« -Initialisiererliste lassen sich schön im Artikel [1] nachlesen. Neu ist hingegen, dass sich in C++ ein spezieller Konstruktor schreiben lässt, der »{}« -Initialisiererlisten annehmen kann: der Sequenzkonstruktor.

Der Sequenzkonstruktor

Sein typisches Einsatzgebiet ist es, eine Klasse zu initialisieren, die selbst einen Container enthält. Listing 1 liefert ein Beispiel für einen Sequenzkonstruktor. Die Klasse »sequence« ist ein Klassen-Template, denn Zeile 6 parametrisiert sie über den Typ-Parameter »T« . Ihre Aufgabe ist es, ihre Daten in dem Vektor »std::vector<T> data« (Zeile 21) zu speichern und auf Anfrage (Zeilen 15 bis 18) auszugeben.

Die Klasse besitzt einen Default-Konstruktor in Zeile 10, einen Sequenzkonstruktor in Zeile 11, der »data« direkt initialisiert, und eine »appendElements« -Methode, die »data« um die Elemente von »inList« erweitert. Dabei sorgt die »std::vector« -Methode dafür, dass Zeile 13 die Elemente der Initialisiererliste »inList« hinter »data.end()« auf den Container »data« schieben kann.

Die »main()« -Funktion ist schnell erklärt: Die Zeilen 26 bis 29 definieren verschiedene »sequence()« -Instanzen und initialisieren sie mit Ausnahme von »sequence<std::string> seqStrings« direkt. Mit »appendElements()« werden die Container in den Zeilen 31 bis 35 sukzessive erweitert. Die unspektakuläre Ausgabe des Programms zeigt Abbildung 1.

Abbildung 1: Das laufende Programm aus <a href="#article_l1" class="listing" title=

Listing 1, das ein Sequenzkonstruktor für ein Klassen-Template implementiert hat.” width=”300″ height=”156″ /> Abbildung 1: Das laufende Programm aus Listing 1, das ein Sequenzkonstruktor für ein Klassen-Template implementiert hat.

Listing 1

sequenceConstructor.cpp

01 #include <initializer_list>
02 #include <iostream>
03 #include <string>
04 #include <vector>
05
06 template <typename T>
07 class sequence{
08
09   public:
10     sequence()= default;
11     sequence(std::initializer_list<T> inList):data(inList){}
12     void appendElements(std::initializer_list<T> inList){
13       data.insert(data.end(),inList);
14     }
15     void showMe() const {
16       for (auto e: data) std::cout << e << " ";
17       std::cout << std::endl;
18     }
19
20   private:
21     std::vector<T> data;
22 };
23
24 int main(){
25   std::cout << std::endl;
26   sequence<char> seqChars={'a','b','c','d','e','f','g'};
27   sequence<int> seqInts= {1,2,3,4,5};
28   sequence<double> seqDoubles={};
29   sequence<std::string> seqStrings;
30
31   seqChars.appendElements({'h','i','j'});
32   seqInts.appendElements({4,3,2,1});
33   seqDoubles.appendElements({1.1,2.2,3.3});
34   seqStrings.appendElements({"one","two","three"});
35
36   seqChars.showMe();
37   seqInts.showMe();
38   seqDoubles.showMe();
39   seqStrings.showMe();
40   std::cout << std::endl;
41 }

Direkt initialisiert

Anspruchsvolle Klassen wie »Widget« in Listing 2 (Zeilen 3 bis 18) zeichnen sich oft dadurch aus, dass sie viele Klassenmitglieder besitzen. Es ist natürlich notwendig, sie zu initialisieren – die klassische Aufgabe für den Initialisierer des Konstruktors. Der initialisiert seine Elemente zum frühestmöglichen Zeitpunkt direkt nach dem Doppelpunkt (Zeilen 5 bis 7), also bevor der Konstruktorkörper ausgeführt wird. Mit wachsender Anzahl an Konstruktoren schwindet allerdings die Übersicht. Zudem muss der Quelltext die Defaultwerte jedes Mal wiederholen. Zum einen ist das ermüdend und zum andern – und vor allem – extrem fehleranfällig.

War es im klassischen C++ nur erlaubt, statische konstante Klassenelemente integralen Typs direkt zu initialisieren, so gilt diese Einschränkung mit dem modernen C++11 nicht mehr. Damit sind die drei Konstruktoren der Klasse »Widget« (Zeile 3) in der Klasse »WidgetImpro« (Zeile 20) deutlich eleganter formuliert. Ein scharfer Blick und zwei Aktionen genügen, um »Widget« in »WidgetImpro« zu überführen.

Im ersten Schritt lassen sich die Klassenelemente »frame« und »visible« direkt im Klassenkörper (Zeilen 34 und 35) auf ihre Defaultwerte setzen. Die gleiche Strategie ist im zweiten Schritt auch auf die Mitglieder »width« und »height« anwendbar, denn die Initialisierung im Konstruktor direkt nach dem Doppelpunkt besitzt Vorrang vor der im Klassenkörper.

Die Ausgabe in Abbildung 2 zeigt: Beide Klassen verhalten sich identisch – ein klassischer Fall von Refaktorierung. Refaktorierung bezeichnet das Verbessern eines bereits funktionierenden Codes unter Beibehaltung seiner Funktionalität. Ziel ist es, die Wartbarkeit, Verständlichkeit und Erweiterbarkeit des Codes zu verbessern. Die folgenden Punkte sind der Refaktorierung zuträglich:

  • Eine Testabdeckung, um Fehler bei geändertem Code sofort zu lokalisieren.
  • Eine integrierte Entwicklungsumgebung, die das Refaktorieren automatisch erledigt.
  • Streng typisierte Sprachen, in denen der Compiler Fehler beim Umbau des Codes sofort moniert.

Dies lässt sich alles in dem gut geschriebenen Wikipedia-Artikel [2] nachlesen.

Abbildung 2: Klassische und direkte Initialisierung von Klassenelementen führen zu gleichen Ergebnissen.

Abbildung 2: Klassische und direkte Initialisierung von Klassenelementen führen zu gleichen Ergebnissen.

Listing 2

directInitialization.cpp

01 #include <iostream>
02
03 class Widget{
04   public:
05     Widget(): width(640), height(480),frame(false), visible(true) {}
06     Widget(int w): width(w), height(getHeight(w)),frame(false),visible(true){}
07     Widget(int w, int h): width(w), height(h), frame(false),visible(true){}
08     void show(){ std::cout << std::boolalpha << width << "x" << height
09                            << ", frame: " << frame << ", visible: " << visible
10                            << std::endl;
11      }
12   private:
13     int getHeight(int w){ return w*3/4; }
14     int width;
15     int height;
16     bool frame;
17     bool visible;
18 };
19
20 class WidgetImpro{
21   public:
22     WidgetImpro(){}
23     WidgetImpro(int w): width(w), height(getHeight(w)){}
24     WidgetImpro(int w, int h): width(w), height(h){}
25     void show(){ std::cout << std::boolalpha << width << "x" << height
26                            << ", frame: " << frame << ", visible: " << visible
27                            << std::endl;
28     }
29
30   private:
31     int getHeight(int w){ return w*3/4; }
32     int width= 640;
33     int height= 480;
34     bool frame= false;
35     bool visible= true;
36 };
37
38 int main(){
39   std::cout << std::endl;
40   Widget wVGA;
41   Widget wSVGA(800);
42   Widget wHD(1280,720);
43
44   wVGA.show();
45   wSVGA.show();
46   wHD.show();
47
48   std::cout << std::endl;
49   WidgetImpro wImproVGA;
50   WidgetImpro wImproSVGA(800);
51   WidgetImpro wImproHD(1280,720);
52
53   wImproVGA.show();
54   wImproSVGA.show();
55   wImproHD.show();
56   std::cout << std::endl;
57 }

Mach du das für mich

Auch das Delegieren und das Vererben von Konstruktoren spart Tipparbeit. Die Grundidee der Delegation von Konstruktoren ist simpel: Ein Konstruktor delegiert seine Arbeit an einen anderen, besser geeigneten. Besser heißt für Listing 3 konkret, dass der Konstruktor in den Zeilen 6 bis 9 seine Argumente analysiert und gegebenenfalls modifiziert.

Die Aufgabe, die die Klasse »Degree« erfüllen soll, ist schnell skizziert. Instanzen von »Degree« repräsentieren Winkel, die auf Winkel zwischen 0 und 360 Grad zu normieren sind. Darüber hinaus rundet der Code die Winkel auf die nächsthöhere Zahl. In Abbildung 3 ist die Umrechnung grafisch dargestellt.

Die Klasse »Degree« besitzt drei Konstruktoren. Der erste (Zeilen 6 bis 9) leistet die Hauptarbeit, denn er normiert die Winkel auf 0 bis 360 Grad. Der Default-Konstruktor in Zeile 11 initialisiert seinen Winkel auf 0 Grad. Er delegiert sein Aufgabe im Initialisierer an den ersten Konstruktor direkt nach dem Doppelpunkt. Das ist genau genommen nicht notwendig, stellt aber sicher, dass der Default-Konstruktor die ganze Funktionalität des ersten Konstruktors verwendet, falls dieser in Zukunft erweitert wird.

Der dritte Konstruktor (Zeile 12) nimmt eine Fließkommazahl an und delegiert seine Aufgabe ebenfalls an den ersten Konstruktor. Dabei rundet er sein Argument auf die nächsthöhere natürliche Zahl: »static_cast<int>(ceil(deg))« . Abbildung 4 zeigt das Programm in Aktion.

Abbildung 3: Instanzen von »Degree« repräsentieren Winkel, die das Programm auf Winkel zwischen 0 und 360 Grad normieren soll.

Abbildung 3: Instanzen von »Degree« repräsentieren Winkel, die das Programm auf Winkel zwischen 0 und 360 Grad normieren soll.

Abbildung 4: Das ablaufende Programm aus <a href="#article_l3" class="listing" title=

Listing 3, das die Delegation von Konstruktoren demonstriert, rechnet Winkel um.” width=”300″ height=”179″ /> Abbildung 4: Das ablaufende Programm aus Listing 3, das die Delegation von Konstruktoren demonstriert, rechnet Winkel um.

Listing 3

delegationConstructor.cpp

01 #include <cmath>
02 #include <iostream>
03
04 class Degree{
05 public:
06   Degree(int deg){
07     degree= deg%360;
08     if ( degree < 0 ) degree+= 360;
09   }
10
11   Degree(): Degree(0){}
12   Degree(double deg): Degree( static_cast<int>(ceil(deg))) {}
13   int getDegree() const { return degree; }
14
15 private:
16   int degree;
17 };
18
19 int main(){
20   std::cout << std::endl;
21   Degree degree10(10);
22   Degree degree45(45);
23   Degree degreeMinus315(-315);
24   Degree degree405(405);
25   Degree degree;
26   Degree degree44(44.45);
27   std::cout << "Degree(10): " << degree10.getDegree() << std::endl;
28   std::cout << "Degree(45): " << degree45.getDegree() << std::endl;
29   std::cout << "Degree(-315): " << degreeMinus315.getDegree() << std::endl;
30   std::cout << "Degree(405): " << degree405.getDegree() << std::endl;
31   std::cout << "Degree(): " << degree.getDegree() << std::endl;
32   std::cout << "Degree(44.45): " << degree44.getDegree() << std::endl;
33   std::cout << std::endl;
34 }

Konstruktoren-Vererbung leicht gemacht

Durch die »using« -Deklaration erbt eine Klasse alle Konstruktoren ihrer direkten Basisklasse mit Ausnahme der Default-, Copy- und Move-Konstruktoren [3]. Damit stehen ihr alle Konstruktoren der Basisklasse zur Verfügung. In Listing 4 (Zeilen 15 bis 21) erbt die Klasse »Derived« durch den Aufruf »using Base::Base« die Konstruktoren der Klasse »Base« (Zeilen 4 bis 13). So lässt sich »Derived« zusätzlich mit einer natürlichen Zahl (Zeile 25) und einem String (Zeile 27) instanzieren. Selbstverständlich steht auch der »Derived« -Konstruktor bereit. Abbildung 5 zeigt, welche Konstruktoren der Compiler implizit verwendet.

Zwei Regeln gilt es beim Vererben von Konstruktoren im Blick zu behalten. Zum einen erbt die abgeleitete Klasse alle Konstruktoren der Basisklasse einschließlich ihrer Charakteristiken, insbesondere der Zugriffsbeschränkungen »public« , »protected« und »private« . Zum anderen wird ein Konstruktor nicht vererbt, falls eine abgeleitete Klasse einen Konstruktor mit gleichen Parametern besitzt.

Abbildung 5: Das Programm aus <a href="#article_l4" class="listing" title=

Listing 4 zeigt, welchen Konstruktor der Compiler jeweils verwendet hat.” width=”300″ height=”165″ /> Abbildung 5: Das Programm aus Listing 4 zeigt, welchen Konstruktor der Compiler jeweils verwendet hat.

Listing 4

inheritingConstructor.cpp

01 #include <iostream>
02 #include <string>
03
04 class Base{
05   public:
06     Base()= default;
07     Base(int i){
08       std::cout << "Base::Base("<< i << ")" << std::endl;
09     }
10     Base(std::string s){
11       std::cout << "Base::Base("<< s << ")" << std::endl;
12     }
13 };
14
15 class Derived: public Base{
16   public:
17     using Base::Base;
18     Derived(double d){
19       std::cout << "Derived::Derived("<< d << ")" << std::endl;
20     }
21 };
22
23 int main(){
24   std::cout << std::endl;
25   Derived(2011);
26   Derived("C++11");
27   Derived(0.33);
28   std::cout << std::endl;
29 }

Wie geht’s weiter?

Praktisch! C++11 bringt zwei neue Literale: Raw-String- und Benutzer-definierte. Erlauben es Raw-String-Literale, das Interpretieren von Slashes »\« in Zeichenketten zu unterdrücken, so ermöglichen es benutzerdefinierte Literale, eigene Literale wie »15_km« , »60.5_sec« oder »45_deg« zu definieren. Wird nun noch ein bisschen C++-Magie beigemischt, wertet der Compiler Ausdrücke der Form »15_km + 5_m – 3_cm« automatisch aus. Wie das genau funktioniert, zeigt die nächste Folge dieser Reihe zum modernen C++.

Infos

  1. Rainer Grimm, “Neue Ausdruckskraft”: Linux-Magazin 02/14, S. 104
  2. Refaktorierung: http://de.wikipedia.org/wiki/Refactoring
  3. Rainer Grimm, “Rasch verschoben”: Linux-Magazin 12/12, S. 96

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