In C++11 lässt sich manches prägnanter formulieren als in klassischem C++. Dieser Artikel zeigt, wie die neue Range-basierte For-Schleife und die automatische Typableitung dabei helfen.
Einfache Dinge sollten einfach, schwierige Dinge möglich sein. Was wie die oberste Maxime jeder Programmiersprache klingt, war in C++ lange keine Selbstverständlichkeit. Dank der Range-basierten For-Schleife und der automatischen Typableitung in C++11 gehört das aber der Vergangenheit an. Standardaufgaben wie das Arbeiten mit einem Container gehen mit dem starken Duo deutlich einfacher von der Hand.
Die harte Tour
In klassischem C++11 gestalten sich das Füllen eines Containers und das Ausgeben seiner Elemente sehr wortreich. Listing 1 exerziert dies für den sequenziellen »std::vector« und den assoziativen Container »std::unordered_map« durch. Details zu den assoziativen Containern lassen sich im vorigen Artikel [1] dieser Serie nachlesen.
Die Arbeit beginnt mit dem Füllen des Containers. Dazu muss der Programmierer beim »std::vector« jedes einzelne Element mit »push_back()« (Zeilen 12 bis 16) auf den Vektor schieben. Zwar besorgt diese Operation den notwendigen Speicher für die neuen Elemente, sie ist aber unangenehm wortreich.
Listing 1
std::vector und std::unordered_map ausgeben
01 #include <iostream>
02 #include <string>
03 #include <unordered_map>
04 #include <utility>
05 #include <vector>
06
07 int main(){
08
09 std::cout << std::endl;
10
11 std::vector<int> myVec;
12 myVec.push_back(1);
13 myVec.push_back(2);
14 myVec.push_back(3);
15 myVec.push_back(4);
16 myVec.push_back(5);
17
18 std::vector<int>::const_iterator vecIt;
19 std::vector<int>::const_iterator vecItEnd= myVec.end();
20
21 for (vecIt= myVec.begin(); vecIt != vecItEnd; ++vecIt){
22 std::cout << *vecIt << " ";
23 }
24
25 std::cout << "\n\n";
26
27 std::unordered_map<std::string,int> myUnordMap;
28 myUnordMap.insert(std::pair<std::string,int>("Dijkstra",1972));
29 myUnordMap.insert(std::make_pair("Scott",1976));
30 myUnordMap["Ritchie"]= 1983;
31
32 std::unordered_map<std::string,int>::const_iterator unordIt;
33 std::unordered_map<std::string,int>::const_iterator unordEnd= myUnordMap.end();
34
35 for (unordIt= myUnordMap.begin(); unordIt != unordEnd; ++unordIt){
36 std::cout << unordIt->first << ": " << unordIt->second << std::endl;
37 }
38
39 std::cout << std::endl;
40
41 }
Container füllen
Der neue Container »std::unordered_map« ist mit der klassischen Syntax ähnlich kompliziert zu füllen. Für jedes Schlüssel-Wert-Paar ist eine eigene Anweisung notwendig (Zeilen 28 bis 30). Sind die Container gefüllt, geben die Zeilen 21 und 35 ihren Inhalt aus. Auch das gestaltet sich sehr umständlich.
Bei »std::vector« , dem sequenziellen Container, definiert Zeile 18 einen konstanten Iterator für den Vektor. Der Programmierer initialisiert ihn in der For-Schleife in Zeile 21 mit dem Ausdruck »vecIt= myVec.begin()« für den Beginn des Vektors. Nun findet die Iteration bis zum Ende des Vektors »vecIt != vecItEnd« statt. Damit man den End-Vektor »vecItEnd« verwenden kann, muss dieser die Position direkt hinter dem Vektor referenzieren. Das erledigt in Zeile 19 die Zuweisung:
std::vector<int>::const_iterator vecItEnd=myVec.end()
Der Rest der For-Schleife ist schnell erklärt: Die Inkrement-Operation »++vecIt« (Zeile 21) setzt den Iterator jeweils um eine Position weiter, der Ausdruck »*vecIt« in Zeile 22 dereferenziert die einzelnen Elemente und gibt sie aus.
Noch aufwändiger gestaltet sich die Ausgabe des assoziativen Containers in den Zeilen 32 bis 37. Dabei folgt sie dem gleichen Kochrezept: Der Code definiert gleich zu Anfang zwei Iteratoren (Zeilen 32 und 33). Dabei ist »unordIt« ein konstanter Iterator, der in der For-Schleife über alle Elementen iteriert, »unordEnd« verweist hinter das Ende des Containers. Da die Elemente des assoziativen Containers Paare sind, referenziert das Programm sie mit den Bezeichnern »first« für den Schlüssel und »second« für den Wert. Mit viel Code entsteht so das bisschen Ausgabe in Abbildung 1.
Kompakter
Das ist aber erst die halbe Geschichte in C++. Um Zeilen wie 18 bis 23 deutlich kompakter zu schreiben, hat sich für sequenzielle Container ein Idiom etabliert, das zwar kürzer, aber auch schwerer verständlich ist. Mit dem Ausdruck
std::copy(myVec.begin(),myVec.end(), std::ostream_iterator<int>(std::cout," "))
lassen sich alle Elemente des Vektors in einer Zeile ausgeben. Dabei kopiert der Algorithmus »std::copy()« die Elemente von »myVec.begin« bis »myVec.end« auf den Ausgabe-Iterator »std::ostream_iterator« . Der ist mit der Ausgabe »std::cout« verbunden und schreibt auf sie alle Zahlen, durch Leerzeichen getrennt.
Schon einfacher machen einige C++11-Features das in Listing 2. Ähnlich kompakt wie der Sourcecode gestaltet sich die Erklärung des einfachen Programms. In den Zeilen 10 und 15 initialisiert es den sequenziellen Container »std::vector« und den assoziativen »std::unordered_map« -Container mit deren Elementen. Dazu kommen Initialisierer-Listen mit geschweiften Klammern zum Einsatz. Der einzige Unterschied zwischen den beiden Initialisierungen besteht darin, dass der sequenzielle Container durch Elemente, der assoziative durch Schlüssel-Wert-Paare initialisiert wird.
Die Range-basierte For-Schleife macht in Kombination mit der automatischen Typableitung durch »auto« die Ausgabe der Container (Zeilen 11 und 16) kurz und prägnant. In dem Ausdruck
for ( auto myInt: myVec ) std::cout <<myInt << " "
erzeugt »auto myInt« implizit den Iterator »myInt« , der alle Elemente des Vektors »myVec« durchläuft. Die einzige Variation zur Ausgabe des assoziativen Containers in Zeile 16 besteht darin, dass auf dessen Schlüssel-Wert-Paare die Bezeichner »myPair.first« und »myPair.second« zugreifen. In Abbildung 1 ist die Ausgabe des Programms zu sehen. Der wortreiche Code aus Listing 1 hat sich in Listing 2 auf das Notwendigste reduziert.
Listing 2
Ausgabe mit C++11-Features
01 #include <iostream>
02 #include <string>
03 #include <unordered_map>
04 #include <vector>
05
06 int main(){
07
08 std::cout << std::endl;
09
10 std::vector<int> myVec{1,2,3,4,5};
11 for ( auto myInt: myVec ) std::cout << myInt << " ";
12
13 std::cout << "\n\n";
14
15 std::unordered_map<std::string,int> myUnordMap{{"Dijkstra",1972},{"Scott",1976},{"Ritchie",1983} };
16 for ( auto myPair: myUnordMap ){
17 std::cout << myPair.first << ": " << myPair.second << std::endl;
18 }
19
20 std::cout << std::endl;
21
22 }
Alles wird besser
Einfache Dinge sollten einfach, schwierige Dinge möglich sein. Die erste Antwort hat dieser Artikel schon gegeben, die zweite ist er noch offen. Mit der Range-basierten For-Schleife lassen sich die Elemente des Containers nicht nur ausgeben, sondern auch an Ort und Stelle modifizieren. Schön zeigt dies Listing 3. Der Code ist leicht verdaulich. Jeder Container durchläuft den gleichen Dreischritt: Zuerst initialisiert ihn das Programm (Zeilen 12, 18 und 24), dann modifiziert es ihn (Zeilen 13, 19 und 25) und schließlich gibt es ihn aus (Zeilen 14, 20 und 26).
Beim Initialisieren des »std::array« mit einer »{}« -Initialisierer-Liste zur Compile-Zeit kommen in Zeile 12 zwei Paare geschweifter Klammern zum Einsatz. Das ist kein Schreibfehler, sondern bei C++11 syntaktisch notwendig, da ein »std::array« ein Aggregat in einem Aggregat darstellt. Doch 2014 steht mit C++14 eine Überarbeitung des C++11-Standards vor der Tür, nach der man die doppelten geschweiften Klammern auch weglassen darf.
Listing 3
Modifizieren von Containern
01 #include <cctype>
02 #include <cmath>
03 #include <array>
04 #include <iostream>
05 #include <string>
06 #include <vector>
07
08 int main(){
09
10 std::cout << std::endl;
11
12 std::array<int,10> myArray{{1,2,3,4,5,6,7,8,9,10}};
13 for ( auto& myInt: myArray ) myInt *= myInt;
14 for ( auto myInt: myArray ) std::cout << myInt << " ";
15
16 std::cout << "\n\n";
17
18 std::vector<double> myVec{1,2,3,4,5,6,7,8,9,10};
19 for ( auto& myDou: myVec ) myDou = std::sqrt(myDou);
20 for ( auto myDou: myVec ) std::cout << myDou << " ";
21
22 std::cout << "\n\n";
23
24 std::string myString{"The next C++ standard is planned for 2014."};
25 for ( auto& c: myString ) c= std::toupper(c);
26 std::cout << myString << std::endl;
27
28 std::cout << std::endl;
29
30 }
Auf der Stelle
Ist das Array initialisiert, kann der Programmierer die Elemente modifizieren, indem er jedem Element sein Quadrat zuweist. Dazu ist es allerdings notwendig, dass er die Aktion direkt auf dem Element und nicht auf dessen Kopie durchführt. Das ist der Grund, warum die Range-basierte For-Schleife sie in »for ( auto& myInt: myArray )« mittels Referenz annimmt. Das gilt auch für die Elemente des Vektors in Zeile 19 und die des Strings in Zeile 25.
Der Code bildet die Vektorelemente auf ihre Wurzel und die Buchstaben des Strings auf ihr großgeschriebenes Pendant ab. Abbildung 2 zeigt die Ausgabe des Programms. Die Mächtigkeit der automatischen Typableitung mit »auto« beschränkt sich nicht nur auf die Range-based For-Schleife. Das Schlüsselwort »auto« sorgt dafür, dass C++11 den Typ automatisch aus dem Initialisierungskontext ermittelt. Das ist insbesondere dann praktisch, wenn der konkrete Typ nur sehr schwer bestimmbar ist.
Die aktuelle Zeit oder Zeitdifferenz zu bestimmen ist mit »auto« in den Zeilen 16 und 46 von Listing 4 ein Kinderspiel. Nicht so hingegen in Zeile 45. Das Gleiche gilt für die Definition und Initialisierung des Funktionszeigers in den Zeilen 18 und 19. Mit »auto« lässt es sich elegant erledigen. Weiter geht es mit den Lambda-Funktionen in Zeile 27 und 28. Den Typ explizit zu definieren ist deutlich anspruchsvoller, als einfach »auto« zu verwenden.
Listing 4
Automatische Typableitung mit auto
01 #include <chrono>
02 #include <iostream>
03 #include <typeinfo>
04
05 int quad(int a){ return a*a; }
06
07 template <typename T1, typename T2>
08 auto add(T1 fir, T2 sec) -> decltype(fir + sec){
09 return fir + sec;
10 }
11
12 int main(){
13
14 std::cout << std::endl;
15
16 auto begin= std::chrono::system_clock::now();
17
18 int(*funcPtrCpp)(int);
19 funcPtrCpp= &quad;
20 auto funcAuto= quad;
21
22 std::cout << "funcPtrCpp(5): " << funcPtrCpp(5) << std::endl;
23 std::cout << "funcAuto(5): " << funcAuto(5) << std::endl;
24
25 std::cout << std::endl;
26
27 int (*addCpp)(int,int)= [](int a, int b){ return a+b;};
28 auto addAuto= [](int a, int b){ return a+b;};
29
30 std::cout << "addCpp(2000,11): " << addCpp(2000,11) << std::endl;
31 std::cout << "addAuto(2000,11): " << addAuto(2000,11) << std::endl;
32
33 std::cout << std::endl;
34
35 auto a= add(2000,11);
36 auto b= add(2000L,11);
37 auto c= add(3,0.1415);
38
39 std::cout << "a: " << a << " of type " << typeid(a).name() << std::endl;
40 std::cout << "b: " << b << " of type " << typeid(b).name() << std::endl;
41 std::cout << "c: " << c << " of type " << typeid(c).name() << std::endl;
42
43 std::cout << std::endl;
44
45 std::chrono::system_clock::time_point end= std::chrono::system_clock::now();
46 auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - begin);
47 std::cout << elapsed.count() << " microseconds " << '\n';
48
49 std::cout << std::endl;
50
51 }
Typfrage
Ein besonderes Einsatzgebiet für »auto« ist die Kombination mit dem neuen Schlüsselwort »decltype« [2], denn beide zusammen erlauben es, den Rückgabetyp einer Funktion automatisch zu ermitteln. Genau dies tut das Funktions-Template »add« in den Zeilen 7 bis 10 von Listing 4. Die Anwendung der Funktion ist dank »auto« (Zeilen 35 bis 37) vollkommen generisch, da der Rückgabetyp nicht spezifiziert werden muss.
Dies gilt, obwohl das Funktions-Template verschiedene Typen zurückgibt. So besitzt »add(2000,11)« ein Ergebnis vom Typ »int« , die beiden folgenden Funktionsaufrufe haben hingegen den Rückgabetyp »long« und »double« . Die »typeid« -Aufrufe in den Zeilen 39 und 41 geben den Typ aus (Abbildung 3). Wie die beiden Schlüsselwörter »auto« und »decltype« zusammenarbeiten, um den Rückgabetyp des Funktionstemplate »add« direkt zu bestimmen, lässt sich am besten anhand von Abbildung 4 erklären.
Der Ausgangspunkt ist das erste Funktionstemplate, in dem der fehlende Rückgabetyp durch drei Fragezeichen »???« angedeutet ist. In dem anschließenden modifizierten Funktionstemplate leitet »auto« (1) den so genannten verzögerten Rückgabetyp ein – verzögert, da dieser erst mit dem Pfeil (2) folgt. Dabei ermittelt das Schlüsselwort »decltype« (3) aus dem Ausdruck »fir + sec« (4) den konkreten Typ des Rückgabewerts.
Zugegeben, die Syntax ist zwar mächtig, aber redundant. Schließlich muss der Programmierer im »decltype« -Ausdruck den Ausdruck hinter der Return-Anweisung nochmals angeben. Das wird sich aber mit C++14 ändern.
2014 wird ein wichtiges Jahr für die Weiterentwicklung von C++11. Mit dem Standard C++14 werden die Neuerungen aus C++11 den letzten Feinschliff erhalten. Das betrifft sowohl die Kernsprache mit generischen Lambda-Funktionen und der vereinfachten Ermittlung des Rückgabetyps einer Funktion als auch die neue Multithreading-Funktionalität mit Reader-Writer-Locks und die Standardbibliothek. Beispielsweise wird der Umgang mit Smart Pointern und Tupeln mächtiger. Grund genug, um im nächsten Artikel dieser Reihe in die C++-Kristallkugel zu schauen und einen Blick in die Zukunft zu wagen.

Abbildung 3: Verschiedene Anwendungen von »auto«.
Infos
- Rainer Grimm, “Geschwindigkeit zählt”: Linux-Magazin 12/13, S. 100
- »decltype« : http://en.cppreference.com/w/cpp/language/decltype









