Gebrauchstauglichkeit oder auch einfach Usability ist ein wichtiges Design-Ziel von C++11. Genau dieses Design-Ziel verfolgen die neuen Features Range-basierte For-Schleife, automatische Typableitung, Lambda-Funktionen und die vereinheitlichte Initialisierung, die Sie in diesem Kapitel genauer kennenlernen werden.
Jeder Datentyp, für den begin()
und end()
so definiert sind, dass er Iteratoren zurückgibt, unterstützt die Range-basierte For-Schleife. Das sind insbesondere alle STL-Container, std::string
, die neuen Datentypen std::array
und Initialisiererlisten.
auto
In Kombination mit auto
lässt sich so äußerst kompakt über einen Bereich (range) iterieren.
for (auto x : {1,2,3,5,8,13,21,34}) std::cout << x << " ";
Werden die Elemente des Bereichs per Referenz angenommen, können sie direkt modifiziert werden. In Kapitel 3, Abschnitt „Die Range-basierte For-Schleife“, sind viele Anwendungsfälle für die Range-basierte For-Schleife zu finden.
Dabei ist ein Ausdruck der Form
for (iterVariable: expression) statement
im Wesentlichen äquivalent zu (Komitee, 2008):
{ auto&& range= expression; for (auto begin= begin(range), end= end(range); begin != end; ++begin){ iterVariable= *begin statement } }
Mit der Range-basierten For-Schleife kann man durch einen String iterieren und ihn modifizieren (Listing 6.1).
forLoop.cpp
01 #include <cctype> 02 #include <iostream> 03 #include <string> 04 05 int main(){ 06 07 std::cout << std::endl; 08 09 // initial string 10 std::string testStr{"Only for Testing Purpose."}; 11 for (auto c: testStr) std::cout << c; 12 std::cout << std::endl; 13 14 // each character upper 15 for (auto& c: testStr) c=std::toupper(c); 16 for (auto c: testStr) std::cout << c; 17 std::cout << std::endl; 18 19 // switch each character from upper to lower case and vice versa 20 testStr= {"Only for Testing Purpose."}; 21 for (auto& c: testStr) c= std::isupper(c)? std::tolower(c): std::toupper(c); 22 for (auto c: testStr) std::cout << c; 23 std::cout << std::endl; 24 25 std::cout << std::endl; 26 27 }
In Listing 6.1 wird die neue For-Schleife sowohl für die Ausgabe (Zeile 11) als auch für die Modifikation des Strings (Zeilen 15 und 21) verwendet. Dabei gibt der ternäre Ausdruck in Zeile 21 den Groß- oder Kleinbuchstaben des Zeichens c
abhängig davon zurück, ob das Prädikat std::isupper(c)
zu wahr oder falsch evaluiert.
Die Ausgabe zeigt den Original-String, den String in Großbuchstaben und den String, bei dem alle Groß- zu Kleinbuchstaben und Klein- zu Großbuchstaben werden.
myVec.cpp myVecSolution.cpp
Aufgabe 6-1
Implementieren Sie einen Container, der in einer Range-basierten For-Schleife verwendet werden kann.
Der Datentyp MyVec
soll eine einfache Hülle um einen std::vector
sein und in einer Range-basierten For-Schleife die Elemente des std::vector
ausgeben. Dazu muss er die Methoden begin()
und end()
anbieten, die Iteratoren zurückgeben.
Als Ausgangsbasis soll Listing 6.2 dienen.
#include <initializer_list> #include <iostream> #include <vector> template <typename T> class MyVec{ public: MyVec(std::initializer_list<T> ele): vec(ele){}; private: std::vector<T> vec; }; int main(){ MyVec<int> myVec{1,2,3,4,5}; // that should work //for (auto m : myVec) std::cout << m << std::endl; }
Aufgabe 6-2
Inkrementieren Sie in einem sequenziellen Container den Wert jedes Elements.
Ihre Liste std::list<int>
besitzt 20 Elemente mit den Werten 0. Inkrementieren Sie die Werte der Liste sukzessive um 1, sodass die Liste die Werte von 1 bis 20 enthält. Geben Sie die modifizierten Werte der Liste aus.
compRangeForEach.cpp
Aufgabe 6-3
Vergleichen Sie die Range-basierte For-Schleife mit dem STL-Algorithmus std::for_each
.
Iterieren Sie dazu über einen Container der Zahlen von 1 bis 10 und ersetzen Sie jede Zahl durch ihr Quadrat. Lösen Sie die Aufgabe mit der Range-basierten For-Schleife und dem STL-Algorithmus std::for_each
. Geben Sie zur Kontrolle die veränderten Container aus.
Das neue Schlüsselwort auto
ist in Teil I, eingeführt und häufig angewandt worden. Was noch fehlt, sind die Details.
Direkte und Kopierinitialisierung
Sowohl die direkte als auch die Kopierinitialisierung ist mit auto
erlaubt und erzeugt den gleichen Datentyp:
auto myIntDirect(1); auto myIntCopy= 1;
Deklaration mehrerer Variablen
Solange jede Initialisierung den gleichen Typ ergibt, kann auto
verwendet werden, um mehrere Variablen zu definieren.
double d= 5.5; auto f= 5.0, *pf= &f, *pd= &d;
Der kurze Codeschnipsel erklärt eine Variable f
vom Typ double
und zwei Zeiger pf
und pd
auf Variablen vom Typ double
. In der Typdefinition mit mehreren Variablen setze ich explizit voraus, dass der Ausdruck von links nach rechts verarbeitet wird.
Automatische Typableitung in Funktions-Templates
Automatische Typableitung mit auto
ist in C++ kein neues Feature. Das Ableiten der Parameter von Funktions-Templates ist schon lange im Einsatz und folgt den gleichen Regeln wie auto
.
01 template <typename T> 02 void myFunc(T var); 03 ... 04 myFunc(expression); 05 auto var= expression;
So wie das Argument expression
in Zeile 4 den Typ von var
in Zeile 2 bestimmt, so bestimmt ihn expression
für auto var
in Zeile 5.
Der Typ der automatisch abgeleiteten Variablen kann durch eine Referenz und einen Zeiger, aber auch durch die Bezeichner const
, volatile
und static
explizit angegeben werden.
auto.cpp
01 #include <vector> 02 03 int func(int){ return 2011; } 04 05 int main(){ 06 07 auto i= 5; 08 auto& intRef=i; // int& 09 auto* intPoint= &i; // int* 10 const auto constInt= i; // const int 11 volatile auto volInt=i; // volatile int 12 static auto staticInt= 10; // static int 13 14 const std::vector<int> myVec; 15 auto vec = myVec; // std::vector<int> 16 auto& vecRef = vec; // const std::vector<int>& 17 18 int myData[10]; 19 auto v3 = myData; // int* 20 auto& v4 = myData; // int (&)[10] 21 22 auto myFunc= func; // func* 23 auto& myFuncRef= func; // (int)(*)(int) 24 25 }
In Listing 6.4 sind verschiedene Kombinationen des neuen Schlüsselworts auto
mit Referenzen und Zeigern, aber auch mit konstanten, volatilen und statischen Datentypen zu sehen. Der resultierende Datentyp folgt im Kommentar.
Implizite Konvertierung
In den Beispielen gelten die Regeln entsprechend der Typableitung in Funktions-Templates. Nur wenn die Variable als Referenz erklärt wird, wird vecRef
zur Referenz (Zeilen 15 und 16). Die implizite Typkonvertierung (decay to pointer) von einem Array auf einen Zeiger auf einen Datentyp int
(Zeile 19) oder von einer Funktion auf einen Zeiger auf eine Funktion (Zeile 22) kann nur durch eine Referenz (Zeilen 20 und 23) verhindert werden.
autoExplicit.cpp autoExplicitSolution.cpp
Aufgabe 6-4
Die automatische Typableitung mit auto
ist das wohl am häufigsten verwendete Feature aus C++11 in diesem Buch.
In dem kleinen Programm in Listing 6.5 wird auto
exzessiv eingesetzt.
01 #include <chrono> 02 #include <future> 03 #include <map> 04 #include <string> 05 #include <tuple> 06 #include <utility> 07 08 int main(){ 09 10 auto myInts={1,2,3}; 11 auto myIntBegin= myInts.begin(); 12 13 std::map<int,std::string> myMap= {{1,std::string("one")},{2,std::string("two")}}; 14 auto myMapBegin= myMap.begin(); 15 16 auto func= [](const std::string& a){ return a;}; 17 18 auto futureLambda= std::async([](const std::string& s) {return std::string("Hello ") + s;}, std::string("lambda function.")); 19 20 auto begin = std::chrono::system_clock::now(); 21 22 auto pa= std::make_pair(1,std::string("second")); 23 24 auto tup= std::make_tuple (std::string("second"),4,1.1,true,'a'); 25 26 }
Schreiben Sie das Programm um, indem alle Verwendungen von auto
durch den expliziten Typ ersetzt werden. Stellen Sie durch die Übersetzung des Programms sicher, dass die richtigen Typen zum Einsatz kommen. Beachten Sie dabei die zusätzlich benötigten Header-Dateien.
invokeFunction.cpp
invokeFunctionSolution.cpp
Aufgabe 6-5
Funktionsparameter dürfen nicht als auto
deklariert werden.
auto
kann fast überall eingesetzt werden, um den Typ aus der Initialisierung automatisch abzuleiten. Zwar ist es in Zeile 16 in Listing 6.5 möglich, einen Funktionsaufruf durch eine Lambda-Funktion zu definieren, aber Listing 6.6 lässt sich nicht übersetzen.
01 #include <iostream> 02 #include <string> 03 04 void invokeFunction(auto func){ 05 std::cout << "I'm a " << func() << "." << std::endl; 06 } 07 08 std::string myFunction(){ 09 return "function"; 10 } 11 12 struct MyFunctionObject{ 13 std::string operator()(){ 14 return "function object"; 15 } 16 }; 17 18 int main(){ 19 20 invokeFunction([]{ return "lambda function";}); 21 22 invokeFunction(&myFunction); 23 24 invokeFunction(MyFunctionObject()); 25 26 }
Der aktuelle GCC-Compiler moniert das sofort (Abbildung 6.2).
Wie lässt sich das Programm übersetzen und ausführen?
Neben auto
kann der in Teil I, vorgestellte Operator decltype
verwendet werden, um den Typ eines Ausdrucks zur Übersetzungszeit zu bestimmen.
decltype wortreicher als auto
Verwenden wir das Listing 6.4 und ergänzen es um die entsprechenden decltype
-Anweisungen, fällt auf den ersten Blick auf, dass decltype
deutlich wortreicher ist (Listing 6.7).
decltype.cpp
01 #include <vector> 02 03 int func(int){ return 1; } 04 05 int main(){ 06 07 auto i= 5; // int 08 decltype(i) iD= i; // int 09 10 auto& intRef=i; // int& 11 decltype(intRef) intRefD= intRef; // int& 12 13 auto* intPoint= &i; // int* 14 decltype(intPoint) intPointD= intPoint; // int* 15 16 const auto constInt= i; // const int 17 decltype(constInt) constIntD= constInt; // const int 18 19 volatile auto volInt=i; // volatile int 20 decltype(volInt) volIntD= volInt; // volatile int 21 22 static auto staticInt= 10; // static int 23 decltype(staticInt) staticIntD= staticInt;// static int 24 25 const std::vector<int> myVec; 26 27 auto vec = myVec; // std::vector<int> 28 decltype(vec) vecD= vec; //const std::vector<int>& 29 30 auto& vecRef = vec; //const std::vector<int>& 31 decltype(vecRef) vecRefD= vecRef;//const std::vector<int>& 32 33 int myData[10]; 34 35 auto v1 = myData; // int* 36 decltype(v1) v1D= v1; // int (&)[10] 37 38 auto& v2 = myData; // int (&)[10] 39 decltype(v2) v2D= v2; // int (&)[10] 40 41 auto myFunc= func; // func* 42 decltype(myFunc) myFuncD= myFunc; // (int)(*)(int) 43 44 auto& myFuncRef= func; // (int)(*)(int) 45 decltype(myFuncRef) myFuncRefD= myFuncRef;// (int)(*)(int) 46 47 }
Auf den zweiten Blick ist das Bild schon deutlich differenzierter. Der Typ, den decltype
zurückgibt, ist der deklarierte Typ (declared type). Hieraus leitet sich auch der Name des Operators decltype
ab. Daher ist es weder notwendig, Referenzen oder Zeiger bzw. die Bezeichner const
, volatile
und static
wie bei auto
explizit zu spezifizieren, noch ist es nötig, den Vektor (Zeile 20), das Array (Zeile 26) und die Funktion (Zeile 3) per Referenz anzunehmen, um eine implizite Konvertierung wie bei auto
zu verhindern.
Automatischer Rückgabetyp
Das Alleinstellungsmerkmal von decltype
fehlt noch. Durch decltype
ist es möglich, den Rückgabewert von Funktions-Templates automatisch bestimmen zu lassen. In C++ wurde dieses Problem gern über Promotion Traits (siehe Anhang E) gelöst.
Ziehen Sie im Zweifelsfall auto decltype vor.
Während auto
für den einfachen Gebrauch ausgelegt ist, ist decltype
das Werkzeug für den Template-Autor.
Bevor wir uns aber einem klassischen Problem der Template-Programmierung widmen, sollten wir uns zunächst die neue, alternative Funktionssyntax anschauen.
Eine Funktion der Form wie in Listing 6.8
lässt sich nun in einer alternativen Syntax (Listing 6.9) definieren:
Wird der Rückgabetyp in der klassischen Funktionsdeklaration zuerst angegeben, folgt er nur nach der Funktionssignatur und wird mit einem ->
eingeleitet. Die Syntax mag an die Funktionsdeklaration in der Mathematik oder auch an Haskell erinnern. Dies ist nicht das Entscheidende. Entscheidend ist, dass a
und b
zu dem späten Zeitpunkt definiert sind und rechts vom ->
verwendet werden können.
Automatischer Rückgabetyp einer Funktion
Die Mächtigkeit von auto
und decltype
zeigt sich erst, wenn beide neuen Features zusammen mit der alternativen Form, Funktionen zu deklarieren, angewandt werden. Denn mit auto
und decltype
lässt sich der Rückgabetyp einer Funktion automatisch bestimmen (Listing 6.10).
newFunctionSyntax.cpp
01 #include <iostream> 02 03 template <class T> 04 auto getValue(T d)-> decltype(d){ 05 return d; 06 } 07 08 template<typename T1, typename T2> 09 auto add(T1 first, T2 second) -> decltype(first + second){ 10 return first + second; 11 } 12 13 int main(){ 14 15 std::cout << std::endl; 16 17 auto testDouble= getValue(3.4); 18 std::cout << "testDouble: " << testDouble << "\n"; 19 20 auto testString= getValue("I'm a string."); 21 std::cout << "testString: " << testString << "\n"; 22 23 auto a1= add(1,1); 24 auto a2= add(1,2.1); 25 std::cout << "add(1,1)= " << a1 << std::endl; 26 std::cout << "add(1,2.1)= " << a2 << std::endl; 27 std::cout << "add(1000LL,5)= " << add(1000LL,5) << std::endl; 28 29 std::cout << std::endl; 30 }
Während getValue
(Zeile 3 in Listing 6.10) mit der klassischen Template-Syntax auch ausgedrückt werden kann, indem der Template-Parameter T
als Rückgabetyp verwendet wird, zeigt das Funktions-Template add
(Zeile 8) eine ganz neue Funktionalität. Der Rückgabewerttyp des Funktions-Templates wird durch den Compiler für den Ausdruck first + second
(Zeile 9) bestimmt. Das Schlüsselwort auto
in der Typdeklaration (Zeilen 4 und 9) leitet die neue Syntax ein, um den Rückgabetyp verzögert zu evaluieren (trailing return type), und ist daher nicht mit der automatischen Typableitung von auto
zu verwechseln.
Das ganze Programm kommt völlig ohne die Angabe eines Typs aus. Ein Vergleich des Funktions-Templates add
mit der klassischen Umsetzung per Promotion Traits bietet drei nicht zu unterschätzende Vorteile:
Die Typkonvertierung wird durch den Compiler automatisch und richtig vollzogen.
Ein generisches Funktions-Template deckt alle Anwendungsfälle ab.
Neue Datentypen müssen nicht nachträglich in das Promotion-Regelwerk eingepflegt werden.
Die Ausgabe bringt kein überraschendes Ergebnis.
typeid.cpp
Aufgabe 6-6
Implizite Typumwandlungen des Compilers
Machen Sie sich mit den Regeln der impliziten Typumwandlung des Compilers bei arithmetischen Operationen vertraut. Verwenden Sie dazu die neue Syntax, Funktionen zu deklarieren, und fragen Sie den Rückgabetyp mit dem Schlüsselwort typeid
ab. Überprüfen Sie bei jedem Ergebnis, ob es Ihren Erwartungen entspricht.
Ein paar Anregungen:
std::cout << typeid( getType(1,false) ).name() << std::endl; std::cout << typeid( getType('a',1) ).name() << std::endl; std::cout << typeid( getType(false,false) ).name() << std::endl; std::cout << typeid( getType(true,3.14) ).name() << std::endl; std::cout << typeid( getType(1,4.0) ).name() << std::endl;
Dabei ist getType
nach der neuen Funktionssyntax deklariert und soll seine zwei Argumente addieren.
newFunctionSyntaxSolution.cpp
Aufgabe 6-7
Schreiben Sie das generische Funktions-Template add
aus Listing 6.10 so um, dass es exakt die Typen ermittelt.
template<typename T1, typename T2> auto add(T1 first, T2 second) -> decltype(first + second){ return first + second; }
Jetzt bin ich penibel. Das Funktions-Template add
bestimmt nicht genau den Rückgabetyp. Wird es mit zwei Rvalues über add(1,2)
aufgerufen, bestimmt es den Rückgabetyp für zwei Lvalues. Der Grund ist, dass die Rvalues mit first
und second
einen Namen erhalten und somit implizit zu Lvalues werden. Das ist in diesem konkreten Fall wohl kein Problem, kann aber dann zu einem werden, wenn Sie die Argumente verwenden, um eine weitere Funktion mit den exakten Argumenten aufzurufen.
Für die Lösung der Aufgaben müssen Sie wohl Kapitel 8, Abschnitt „Perfect Forwarding“ zurate ziehen. Testen Sie anschließend Ihre Lösung, indem Sie das neue Funktions-Template in Listing 6.10 einbauen.
Teil I, enthält viele Beispiele für Lambda-Funktionen. Lambda-Funktionen unterstützen die Lokalität der Funktionalität, denn genau dort, wo eine aufrufbare Einheit benötigt wird, kann diese direkt definiert werden.
Komponente | Bereich der Lambda-Funktion | |
[ ] | Bindung an die Variablen des lokalen Bereichs | |
[] | keine Bindung | |
[=] | die Werte werden kopiert | |
[&] | die Werte werden referenziert | |
( ) | Argumente des Funktionskörpers (optional) | |
-> | Rückgabewert (optional) | |
{ } | Funktionskörper |
Dabei lässt sich eine Lambda-Funktion als Funktionsobjekt vorstellen, das an Ort und Stelle implizit definiert und ausgeführt wird.
lambdaFunctionObject.cpp
01 #include <algorithm> 02 #include <iostream> 03 #include <vector> 04 05 class AccumTemp{ 06 int& sum; 07 int inc; 08 09 public: 10 11 AccumTemp (int& sum_, int inc_): sum(sum_), inc(inc_) {} 12 13 int operator()(int v) const { 14 return sum += v+inc; 15 } 16 }; 17 18 int main(){ 19 20 std::cout << std::endl; 21 22 std::vector<int> vecInt{1,2,3,4,5,6,7,8,9,10}; 23 int sumLambda=0; 24 int inc=5; 25 26 // summation with the lambda function 27 std::for_each(vecInt.begin(),vecInt.end(), [&sumLambda,inc](int v){sumLambda += v+inc;}); 28 29 std::cout << "Summation with the Lambda Function: " << sumLambda << std::endl; 30 31 std::cout << std::endl; 32 33 int sumFunctionObject=0; 34 35 // summation with the function object 36 std::for_each(vecInt.begin(),vecInt.end(), AccumTemp(sumFunctionObject,inc)); 37 38 std::cout << "Summation with the Function Object: " << sumFunctionObject << std::endl; 39 40 std::cout << std::endl; 41 42 }
Der Aufruf der Lambda-Funktion wie auch der des Funktionsobjekts erzielen das gleiche Ergebnis – 105 (Abbildung 6.4).
Die Lambda-Funktion in Listing 6.12 [&sumLambda,inc](int v){sumLambda += v+inc;}
(Zeile 27) ist über ihren Funktionskörper am einfachsten zu verstehen. In diesem wird die lokale Summationsvariable sumLambda
(Zeile 23) per Referenz adressiert, da der Wert der Summation nach der Schleife zur Verfügung stehen soll. Erreicht wird das dadurch, dass die Bindung mit einer Referenz &sumLambda
in dem Bindungsbereich []
deklariert wird. Hingegen ist es für das lokale inc
(Zeile 5) ausreichend, kopiert zu werden. Das Argument v
der Lambda-Funktion nimmt die Werte des Vektors an.
Lambda-Funktion versus Funktionsobjekte
Ein scharfer Blick auf das Funktionsobjekt und die Lambda-Funktion zeigt die Parallelität auf. Der Funktionskörper der Lambda-Funktion findet sich im überladenen Klammeroperator (Zeile 13) wieder. Sein Argument v
entspricht dem Argument der Lambda-Funktion. Die Summationsvariable sumFunctionObject
wird per Referenz im Objekt gebunden, inc
hingegen kopiert.
Das Referenzieren des lokalen Bereichs im Bindungsbereich []
ist beliebig feingranular spezifizierbar. Neben dem Verhalten »Kopiere oder referenziere alle im Funktionskörper verwendeten Variablen des lokalen Bereichs« lassen sich explizite Ausnahmen festlegen. Dies ist am einfachsten anhand eines kleinen Beispiels erklärt. Für dieses Beispiel sollen die drei Variablen a
, b
und c
deklariert sein und für den Bindungsbereich verschiedene Möglichkeiten durchgespielt werden. Für die drei Variablen ist explizit angegeben, ob sie im Funktionsblock nicht (Ø), per Referenz (&) oder per Kopie (=) zur Verfügung stehen.
Lambda-Funktionen können als Funktionen angesehen werden, die ihren Aufrufkontext konservieren.
Closure und Funktionsabschluss
closure.cpp
01 #include <algorithm> 02 #include <iostream> 03 #include <string> 04 #include <vector> 05 06 07 int main(){ 08 09 std::cout << std::endl; 10 11 std::vector<int> vecInt={1,2,3,4,5,6,7,8,9,10}; 12 13 std::string seperator=""; 14 auto sepEmp= [seperator](int i) {std::cout << i << seperator;}; 15 16 seperator=":"; 17 auto sepColon=[seperator](int i) {std::cout << i << seperator;}; 18 19 seperator="-"; 20 auto sepHyphen=[seperator](int i) {std::cout << i << seperator;}; 21 22 seperator=","; 23 auto sepComma=[seperator](int i) {std::cout << i << seperator;}; 24 25 26 std::for_each(vecInt.begin(),vecInt.end(),sepEmp); 27 std::cout << std::endl; 28 29 std::for_each(vecInt.begin(),vecInt.end(),sepColon); 30 std::cout << std::endl; 31 32 std::for_each(vecInt.begin(),vecInt.end(),sepHyphen); 33 std::cout << std::endl; 34 35 std::for_each(vecInt.begin(),vecInt.end(),sepComma); 36 std::cout << std::endl; 37 38 std::cout << std::endl; 39 40 }
Das konstruierte Listing 6.14 verdeutlicht, wie Lambda-Funktionen ihren Aufrufkontext binden können. Ziel des Programms ist es, den Vektor über natürliche Zahlen mit verschiedenen Trennzeichen auszugeben. Beim Definieren der Lambda-Funktionen in den Zeilen 14, 17, 20 und 23 wird der String seperator
gebunden. Dies ist der Grund dafür, dass jede Iteration über den Vektor mithilfe dieser Lambda-Funktionen ein anderes Trennzeichen anwendet (Zeilen 26, 29, 32 und 35) und ausgibt.
dangling reference
Bindet die Lambda-Funktion die Variablen ihres Aufrufkontexts per Referenz, muss sichergestellt sein, dass die Lambda-Funktion ihre verwendeten Variablen überlebt. Genau das ist in Listing 6.15 nicht der Fall.
danglingReference.cpp
01 #include <functional> 02 #include <iostream> 03 04 std::function<std::string()> makeLambda() { 05 const std::string val="very bad"; 06 return [&val]{ return val;}; 07 } 08 09 int main(){ 10 11 std::cout << std::endl; 12 13 auto bad= makeLambda(); 14 std::cout << bad() << std::endl; 15 16 std::cout << std::endl; 17 18 }
Die Funktion makeLambda
in Listing 6.15 erzeugt eine einfache Lambda-Funktion und gibt diese zurück. Die Lambda-Funktion benötigt kein Argument und soll einen std::string
zurückgeben. Genau so ist der Rückgabewert durch das Funktionsobjekt std::function<std::string()>
definiert. In Zeile 13 wird die Lambda-Funktion an bad
zugewiesen und in der nächsten Zeile ausgeführt. Das Programm besitzt undefiniertes Verhalten. Führt dies mit dem GCC 4.6 (Abbildung 6.6) zu einem Speicherzugriffsfehler, so wird mit dem GCC 4.7 (Abbildung 6.7) der String "very bad"
gar nicht ausgegeben.
Was ist der Grund? In der Lambda-Funktion wird in Listing 6.15 der String val
per Referenz gebunden. Ist die Funktion ausgeführt, endet der Lebenszyklus der Variablen. Die Lambda-Funktion referenziert bei ihrer Ausführung eine Variable, die nicht mehr existiert. Es entsteht eine klassische dangling reference.
Beachten Sie die Lebenszeit von Referenzen in Lambda-Funktionen.
Binden Lambda-Funktionen Variablen per Referenz, muss die Variable die Lambda-Funktion überleben.
Klassenmethode
Will eine Lambda-Funktion, die in einer Methode implementiert ist, auf die Klassenelemente zugreifen, muss sie einen Standardbindungsmodus [&]
oder [=]
oder this
angeben. Damit kann die Lambda-Funktion in ihrem Funktionskörper auf alle Elemente der Klasse per Referenz ([&]
), per Copy ([=]
) oder auf die Elemente des Objekts (this
) zugreifen, unabhängig davon, ob diese privat
, protected
oder public
definiert sind (Listing 6.16).
classMember.cpp
01 #include <iostream> 02 03 class ClassMember{ 04 05 const static int a= 1; 06 07 int get10(){ 08 return 10; 09 } 10 public: 11 void showAll(){ 12 // define and invoke (trailing ()) the lambda functions 13 [this]{std::cout << "by this= " << get10() + a << std::endl;}(); 14 [&]{std::cout << "by reference= " << get10() + a << std::endl;}(); 15 [=]{std::cout << "by copy= " << get10() + a << std::endl;}(); 16 } 17 }; 18 19 int main(){ 20 std::cout << std::endl; 21 22 ClassMember cM; 23 cM.showAll(); 24 25 std::cout << std::endl; 26 }
In Listing 6.16 kann sowohl das private Element a
als auch die private Methode get10()
in den drei Lambda-Funktionen (Zeilen 13, 14 und 15) verwendet werden. Damit die Lambda-Funktion direkt aufgerufen werden kann, kommt auf jedem der drei Funktionsobjekte der ()
-Operator zum Einsatz.
Es fehlt noch das Ergebnis des Programmlaufs:
Lambda-Funktionen, die keine Parameter besitzen, können auf das Klammerpaar () verzichten. Dies ist in den drei Lambda-Funktionen (Zeilen 13, 14 und 15) in Listing 6.16 zu sehen.
Besteht der Funktionskörper einer Lambda-Funktion nur aus einem return Ausdruck;
oder gibt die Lambda-Funktion keinen Wert zurück, kann auf die Angabe des Rückgabetyps verzichtet werden. Die meisten Lambda-Funktionen, die bisher verwendet wurden, machten von dieser Option Gebrauch.
Verlangt eine Lambda-Funktion die explizite Angabe des Rückgabetyps, muss dieser in der alternativen Funktionssyntax angegeben werden.
Lambda-Funktionen in C++11 sind relativ mächtig, verglichen mit Lambda-Funktionen in anderen Programmiersprachen wie Python. Sie können aus mehreren Ausdrücken und sogar Statements bestehen.
Komplexere Lambda-Funktionen verlangen ein geschultes Auge. In Listing 6.17 wird eine Lambda-Funktion erklärt, die als Ergebnis die zwei Argumente addiert und zurückgibt. Die Lambda-Funktion erhält mithilfe von auto
den Namen addLambda
und verhält sich wie die Funktion addFunction:
int addFunction(int x,int y){ int z; z= x+;y return z; }
Einsatz von Lambda-Funktionen
Lambda-Funktionen sind dazu da, die Funktionalität an Ort und Stelle auf den Punkt zu bringen. Werden Lambda-Funktionen wiederverwendet oder bestehen sie aus mehr als einem Ausdruck, sollte darüber nachgedacht werden, die Funktionalität in einer Funktion oder einem Funktionsobjekt anzubieten.
danglingReference.cpp
Aufgabe 6-8
Korrigieren Sie das Programm danglineReference.cpp in Listing 6.15.
Wie müsste danglingReference.cpp in Listing 6.15 implementiert werden, damit das Programm wohldefiniert ist? Hierzu bieten sich einige Variationen an:
Geben Sie den Rückgabewert per Copy zurück.
Verwenden Sie eine globale Variable für den Rückgabewert.
Erweitern Sie makeLambda
um einen Eingabeparameter std::string
.
Erweitern Sie die von makeLambda
erzeugte Lambda-Funktion um einen Eingabeparameter std::string
.
createClosure.cpp
Aufgabe 6-9
Lassen Sie sich ein Closure von einer Klasse zurückgeben.
Die Klasse kann ganz einfach strukturiert sein.
01 class CreateClosure{ 02 public: 03 void setName(const std::string& n){ 04 name=n; 05 } 06 . . . 07 private: 08 std::string name; 09 }; 10 11 12 int main(){ 13 14 std::cout << std::endl; 15 16 CreateClosure creatClos; 17 18 creatClos.setName("first"); 19 auto first= creatClos.getIt(); 20 std::cout << "first(): " << first() << std::endl; 21 22 creatClos.setName("second"); 23 std::cout << "createClos.getIt()(): " << creatClos.getIt()() << std::endl; 24 25 std::cout << std::endl; 26 27 }
Nun soll die Klasse eine Methode erhalten, die auf Anfrage einen Closure zurückgibt. Wird der Closure ausgeführt, gibt er den Wert der Variablen name
zurück. Die entscheidende Zeile 6 fehlt in Listing 6.19. Ausgeführt, soll das Programm die Ausgabe aus Abbildung 6.9 besitzen.
Implementieren Sie ein Funktionsobjekt.
In folgendem Listing ist eine einfache Lambda-Funktion definiert.
const std::string hello("lambda"); auto myLambda= [hello](const std::string& a) {return hello + " " + a; }; std::cout << myLambda("function") << std::endl;
Ausgeführt, gibt die Lambda-Funktion lambda function
aus. Implementieren Sie ein Funktionsobjekt mit der gleichen Funktionalität, das sich wie eine Lambda-Funktion anfühlt.
const std::string helloFunc("function"); auto myFunctionObject= MyFunctionObject(helloFunc); std::cout << myFunctionObject("object") << std::endl;
Vereinheitlichte Initialisierung überall
{}-Initialisiererlisten können in C++11 universell eingesetzt werden. Die Initialisierung mit {}-Listen verdrängt nicht die Initialisierung der Daten in C++98, sondern ergänzt sie. In Teil I, war dieses Feature schon häufig im Einsatz, da das Initialisieren von Containern damit sehr praktisch ist. Ein paar weitere Beispiele, die nicht alle Anwendungsfälle abdecken können, sollen dieses mächtige Feature noch besser veranschaulichen.
uniformInitialization.cpp
01 #include <unordered_map> 02 #include <string> 03 #include <vector> 04 05 struct MyStruct{ 06 int x; 07 double y; 08 }; 09 10 class MyClass{ 11 public: 12 int x; 13 double y; 14 }; 15 16 struct Telephone{ 17 std::string name; 18 int number; 19 }; 20 21 Telephone getTelephone(){ 22 // Telephone("Rainer Grimm",12345) created 23 return {"Rainer Grimm",12345}; 24 } 25 26 struct MyArray { 27 public: 28 MyArray(): data {1, 2, 3, 4, 5} {} 29 private: 30 const int data[5]; 31 }; 32 33 void getVector(const std::vector<int>& v){ 34 // some code 35 } 36 37 int main(){ 38 39 // built-in datatypes and strings 40 bool b{true}; 41 bool b2= true; 42 int i{2011}; 43 int i2= {2011}; 44 std::string s{"string"}; 45 std::string s2= {"string"}; 46 47 // struct and class 48 MyStruct basic{5,3.2}; 49 MyStruct basic2= {5,3.2}; 50 MyClass alsoClass{5,3.2}; 51 MyClass alsoClass2= {5,3.2}; 52 53 // C-Array 54 // dynamic array initialization 55 const float * pData = new const float[4] { 1.5, 4, 3.5, 4.5 }; 56 57 // STL-Container 58 // a vector of 1 element 59 std::vector<int>oneElement{1}; 60 std::vector<int>oneElement2= {1}; 61 62 std::unordered_map<std::string,int> um { {"Dijkstra",1972},{"Scott",1976},{"Wilkes",1967}, {"Hamming",1968} }; 63 std::unordered_map<std::string,int> um2= { {"Dijkstra",1972},{"Scott",1976},{"Wilkes",1967}, {"Hamming",1968} }; 64 // special cases 65 // brace initialization for a std::vector 66 getVector({ oneElement[0],5, 10, 20, 30 }); 67 68 // methode 69 std::vector<int> v {}; 70 v.insert(v.end(), { 99, 88, -1, 15 }); 71 72 // getTelephone returns a initializer list 73 Telephone tel(getTelephone()); 74 }
In Listing 6.20 sind viele verschiedene Variationen der vereinheitlichten Initialisierung dargestellt. In der Regel wird sowohl die direkte als auch die Kopierinitialisierung unterstützt. Diese trifft auf einfache Datentypen (Zeilen 40 bis 45), auf Strukturen und Klassen (Zeilen 48 bis 51), aber auch auf STL-Container (Zeilen 57 bis 63) zu. Sehr interessant ist die Initialisierung des Arguments der Funktion getVector
über eine Initialisiererliste. Der std::vector
erhält mit C++11 eine weitere Methode insert
(Zeile 70), die direkt mit einer Initialisiererliste angesprochen werden kann. In der Funktion getTelephone
in Zeile 23 wird implizit ein Objekt Telephone("Rainer Grimm",12345)
erzeugt. Damit ist es möglich, als Rückgabewert eine Initialisiererliste zu verwenden.
initializerList.cpp
Aufgabe 6-11
Initialisieren Sie verschiedene Container mit Initialisiererlisten.
Initialisieren Sie ein std::array
, ein std::vector
, ein std::set
und ein std::unordered_multiset
durch die {-10,5,1,4,5}
-Initialisiererliste und geben Sie die Elemente aus.
Beachten Sie die feinen Unterschiede:
Wie wird die Länge der Container angegeben?
Werden mehrfach vorkommende Elemente respektiert?
Sind die Elemente im Container sortiert?
Unterscheiden Sie zwischen direkter und Kopierinitialisierung mit Initialisiererlisten.
In Listing 6.20 behaupte ich, dass in der Regel die direkte wie auch die Kopierinitialisierung mit Initialisiererlisten unterstützt werden.
Aufgabe 6-13
Entdecken Sie den feinen Unterschied zwischen der Initialisierung mit den runden () und den geschweiften {} Klammern.
Einen feinen Unterschied gibt es zwischen der Initialisierung mit runden () und der mit geschweiften {} Klammern. Beim bekannten Initialisieren mit den runden Klammern findet gegebenenfalls eine implizite Verengung (narrowing) des Datentyps statt. Bei der Initialisierung mit den eckigen Klammern ist das nicht der Fall.
Machen Sie sich den feinen Unterschied an ein paar Beispielen aus dem Entwurf N3242 von Pete Becker (Becker, 2011) des kommenden C++11 klar.
01 const int y = 999; 02 const int z = 99; 03 char c3{y}; // error: narrows (assuming char is 8 bits) 04 char c4{z}; // OK: no narrowing needed 05 unsigned char uc1 = {5}; // OK: no narrowing needed 06 unsigned char uc2 = {-1}; // error: narrows 07 unsigned int ui1 = {-1}; // error: narrows 08 int ii = {2.0}; // error: narrows 09 float f2 { 7 }; // OK: 7 can be exactly represented as a float
Die Zeilen 4, 5 und 9 stellen intelligente Ausnahmen der Regel vor. Passt der Quelltyp ohne Informationsverlust in den Zieltyp, findet das implizite Verengen des Datentyps auch mit der {}-Initialisiererliste statt.