Aus Linux-Magazin 02/2012

Modernes C++ in der Praxis – Folge 2

Lambda-Funktionen sind die praktischen Helfer der Sprache C++11. Schon nach kurzer Zeit möchte kein C++-Entwickler sie missen, denn mit ihnen ist ein Algorithmus rasch und ohne Umschweife formuliert. Außerdem darf er sie wie Objekte behandeln.

Hinter dem griechischen Buchstaben Lambda verbergen sich in C++11 besondere Funktionen: Funktionen ohne Namen. Als anonyme Ausdrücke verbinden sie das Aufrufverhalten einer Funktion mit einem Gedächtnis.

Ohne Umschweife

Nichts Neues, dürfte mancher C++-Entwickler erwidern, das kann ein Funktionsobjekt [1] auch. Stimmt, denn eine Lambda-Funktion ist nur Syntactic Sugar [2] für ein Funktionsobjekt, aber von der süßesten Art. Denn um ein Funktionsobjekt zu verwenden, sind zwei zusätzliche Arbeitsschritte erforderlich: Zum einen muss der Entwickler die Klasse eines Funktionsobjekts definieren, zum anderen es auch instanzieren.

Diesen insgesamt drei Schritten steht der direkte Einsatz der Lambda-Funktion gegenüber. Dabei folgt diese Funktion der einfachen Struktur in Abbildung 1: Sie besteht aus vier Komponenten. Die eckigen Klammern »[ ]« besitzen eine Doppelfunktion: Zum einen leiten sie die anonyme Funktion ein, zum anderen kann der Programmierer zwischen ihnen den aufrufenden Kontext erfassen.

Abbildung 1: Die Notation einer Lambda-Funktion in C++11. Einige Bestandteile darf der Programmierer weglassen.

Abbildung 1: Die Notation einer Lambda-Funktion in C++11. Einige Bestandteile darf der Programmierer weglassen.

Weiter geht es mit den runden Klammern »( )« . Sie erklären – in Analogie zu einer Funktionsdefinition – die Parameter der Funktion. Erwartet die Lambda-Funktion keine Argumente, so dürfen die runden Klammern entfallen. Entfallen kann auch die Angabe des Rückgabetyps »->« , falls die Lambda-Funktion keinen Wert zurückgibt oder der einfachen Struktur »return Ausdruck;« folgt. Dabei steht der Ausdruck »->int« für einen Rückgabetyp »int« in der neuen, alternativen Funktionssyntax, die bei Lambda-Funktionen obligatorisch ist. Zuletzt folgt in geschweiften Klammern »{ }« der Funktionskörper.

Eine Lambda-Funktion in C++11 kann die Variablen des aufrufenden Kontexts erfassen. Damit besitzt sie ein Gedächtnis und stellt eine Closure dar (siehe Kasten “Closure”). Dieses Erfassen der Variablen geschieht per Kopie oder per Referenz. Der Entwickler entscheidet nach dem Einsatzzweck, welche Variante er benutzt. In diesem Punkt unterscheidet sich die Lambda-Funktion nicht von einer herkömmlichen Funktion.

Closure

Eine Closure beziehungsweise ein Funktionsabschluss ist eine Funktion, die ihren Erzeugungskontext konservieren kann. Damit erlaubt es eine Closure, Variablen in ihrem Funktionskörper zu binden, sodass sie bei der Ausführung der Funktion zur Verfügung stehen. Dabei wird nicht nur der Wert der Variablen konserviert, sondern auch ihre Lebenszeit verlängert.

Kopie oder Referenz

Listing 1 zeigt die beiden Alternativen: Das Beispielprogramm initialisiert den String »copy« (Zeile 6) sowie den String »ref« (Zeile 7) mit dem gleichen Wert »”original”« . Die Lambda-Funktion in Zeile 8 erfasst »copy« per Kopie und »ref« per Referenz. Beim ersten Ausführen der Lambda-Funktion in Zeile 9 sind die beiden Werte unverändert. Wird der Wert der beiden Strings in den Zeilen 10 und 11 auf »”changed”« gesetzt, zeigt sich der Unterschied: Nur die per Referenz eingebundene Variable »ref« gibt beim nochmaligen Ausführen der Funktion »lambda()« in Zeile 12 die Änderung des Wertes wieder (Abbildung 2). Weitere Möglichkeiten, den Kontext zu binden, beschreibt der Kasten “Aufrufkontext erfassen”.

Aufrufkontext erfassen

Eine Lambda-Funktion kann ihren aufrufenden Kontext auf drei Arten erfassen: Die leeren eckigen Klammern »[]« verzichten auf den Zugriff. Das kaufmännische Und-Zeichen »[&]« referenziert alle Variablen, die in der Lambda-Funktionen verwendet werden. Der dritte Modus schreibt sich »[=]« und kopiert alle verwendeten Variablen.

Neben diesen Defaultmodi sind auch alle denkbaren Kombinationen möglich. So bewirkt beispielsweise »[=,&var]« , dass C++11 standardmäßig alle in der Lambda-Funktion verwendeten Variablen kopiert, die Variable »var« hingegen referenziert.

Listing 1

Kopie und Referenz

01 #include <iostream>
02
03 int main(){
04   std::cout << std::endl;
05
06   std::string copy= "original";
07   std::string ref= "original";
08   auto lambda=[copy,&ref]{std::cout << copy << " " << ref << std::endl;};
09   lambda();
10   copy="changed";
11   ref= "changed";
12   lambda();
13
14   std::cout << std::endl;
15 }
Abbildung 2: Eine Lambda-Funktion erfasst Variablen durch Kopie (erste Ergebniszeile) oder durch Referenz (zweites Ergebnis).

Abbildung 2: Eine Lambda-Funktion erfasst Variablen durch Kopie (erste Ergebniszeile) oder durch Referenz (zweites Ergebnis).

Wer die besonderen Regeln zum Erfassen von Variablen verinnerlicht, kann eine »join()« -Funktion in C++11 rasch umsetzen. Die Funktion nimmt einen Vektor von Strings »str« und einen String »sep« als Argumente an, verbindet die Elemente des Vektors mit dem Separator und gibt den resultierenden String als Ergebnis zurück. Wer sich an die gleichnamige Python-Funktion erinnert fühlt, liegt vollkommen richtig.

Die Anwendung der Funktion in Listing 2 ist schnell erklärt: Enthält der Vektor mehr als einen String, verbindet der Trenner dessen Elemente in den Zeilen 22, 25, 28, 31 und 34. Der Defaultwert für den Separator ist der leere String »””« (Zeile 6). Die Ausgaben des Programms zeigt Abbildung 3.

Listing 2

Join-Funktion

01 #include <algorithm>
02 #include <iostream>
03 #include <string>
04 #include <vector>
05
06 std::string join(std::vector<std::string>& str, std::string sep=""){
07
08   std::string joinStr="";
09   if (not str.size()) return joinStr;
10   std::for_each(str.begin(),str.end()-1,
11       [&joinStr,sep](std::string v) {joinStr+= v + sep;});
12   joinStr+= str.back();
13   return joinStr;
14
15 }
16
17 int main(){
18   std::vector<std::string> myVec;
19   std::cout << join(myVec) << std::endl;
20
21   myVec.push_back("One");
22   std::cout << join(myVec) << std::endl;
23
24   myVec.push_back("Two");
25   std::cout << join(myVec) << std::endl;
26
27   myVec.push_back("Three");
28   std::cout << join(myVec,":") << std::endl;
29
30   myVec.push_back("Four");
31   std::cout << join(myVec,"/") << std::endl;
32
33   myVec.push_back("Five");
34   std::cout << join(myVec,"XXX") << std::endl;
35
36   std::cout << std::endl;
37 };
Abbildung 3: Mit der anonymen Funktion ist schnell ein Programm geschrieben, das die Elemente eines Vektors aufreiht und mit Separatorzeichen trennt.

Abbildung 3: Mit der anonymen Funktion ist schnell ein Programm geschrieben, das die Elemente eines Vektors aufreiht und mit Separatorzeichen trennt.

Die Funktion »join()« ist eine genauere Betrachtung wert: Die Variable »joinStr« in Zeile 8 stellt den zusammengefügten String dar. Ist der Vektor leer (Zeile 9), kommt ein leerer String als Ergebnis zurück. Das Zusammenfügen des neuen Strings findet in Zeile 11 statt, in der Lambda-Funktion »[&joinStr,sep](std::string v) {joinStr+= v + sep;}« des »std::for_each« -Algorithmus.

Dabei verarbeitet der Algorithmus jedes Element »v« des Vektors mit Ausnahme des letzten Strings (»str.begin,str.end()-1« ), indem er den String »v« und den Trenner »sep« an den resultierenden String »joinStr« anhängt: »joinStr+= v + sep« .

Zum Abschluss erweitert Zeile 12 die Variable »joinStr« um den letzten String »str.back()« . Dieser Schritt findet auch statt, wenn der Vektor nur aus einem einzigen String besteht. Damit der Funktionskörper der Lambda-Funktion nicht in jedem Schritt von »std::for_each« versucht eine Kopie von »joinStr« zu modifizieren, ist es notwendig, dass sie die Zeichenkette per »[&joinStr,sep]« als Referenz erfasst.

Closures

Eine »join()« -Funktion in C++11 ist bei Weitem noch nicht das Ende der Geschichte. Eine Lambda-Funktion kann der Programmierer nicht nur als Closure verwenden, die ihren Erzeugungskontext erfasst – er darf sie auch wie ein Objekt kopieren. Verknüpft er diese beiden Eigenschaften, erhält er eine besondere Funktion wie beispielsweise »inRange()« (Zeile 6) in Listing 3, die auf Anfrage eine Funktion erzeugt.

Listing 3

Einfaches Erzeugen eines Filters

01 #include <algorithm>
02 #include <functional>
03 #include <iostream>
04 #include <random>
05
06 std::function<bool(int)> inRange(int low, int up){
07   return [low,up](int d){return d >= low and d <= up;};
08 }
09
10 int main(){
11   std::cout << std::boolalpha << std::endl;
12
13   std::cout << "4 inRange(5,10): " << inRange(5,10)(4) << std::endl;
14   auto filt= inRange(5,10);
15   std::cout << "5 inRange(5,10): " << filt(5) << std::endl;
16
17   std::cout << std::endl;
18
19   const int NUM=60;
20   std::random_device seed;
21
22   // generator
23   std::mt19937 engine(seed());
24
25   // distribution
26   std::uniform_int_distribution<int> six(1,6);
27
28   std::vector<int> dice;
29   for ( int i=1; i<= NUM; ++i) dice.push_back(six(engine));
30
31   std::cout << std::count_if(dice.begin(),dice.end(),inRange(6,6)) << " of " << NUM << " inRange(6,6) " << std::endl;
32   std::cout << std::count_if(dice.begin(),dice.end(),inRange(4,6)) << " of " << NUM << " inRange(4,6) " << std::endl;
33
34   // remove all elements for 1 to 4
35   dice.erase(std::remove_if(dice.begin(),dice.end(),inRange(1,4)),dice.end());
36   std::cout << "All numbers 5 and 6: ";
37   for (auto v: dice ) std::cout << v << " ";
38
39   std::cout << "\n\n";
40 }

Die Funktion »inRange()« erwartet zwei Argumente und gibt wiederum eine Funktion zurück. Sie hat den Typ »std::function<bool(int)>« und zeichnet sich dadurch aus, dass sie eine natürliche Zahl annimmt und als Ergebnis einen Wahrheitswert zurückgibt. Derartige Funktionen, die einen Wahrheitswert zurückgeben, werden Prädikat genannt und sind in der Standard Template Library häufig im Einsatz.

So funktioniert das Ganze: Wird die Funktion »inRange(5,10)« aufgerufen, führt dies dazu, dass ihre Lambda-Funktion teilweise evaluiert wird. Der Ausdruck »inRange(5,10)« bindet die Werte von »low« =5 und »up« =10, denn die Lambda-Funktion »[low,up](int d){return d >= low and d <= up;}« ist eine Closure. Das Ergebnis von »inRange(5,10)« ist die Funktion »[](int d){return d >= 5 and d <= 10;}« . Das Ergebnis von »inRange(5,10)(4)« ist »false« , denn »4« in die Lambda-Funktion »[](int d){return d >= 5 and d <= 10;}« eingesetzt, ergibt »false« .

Zeile 14 von Listing 3 bindet die anonyme Funktion an einen Namen, damit sie sich wie eine gewöhnliche Funktion verwenden lässt. Die Mächtigkeit von »inRange()« zeigt sich am besten, wenn man die Funktion auf beliebige Zahlen als Filter anwendet.

Um geeignetes Zahlenmaterial zu erzeugen, regiert in den Zeilen 20 bis 29 der Zufall. Mit der neuen C++11-Funktionalität initialisiert Zeile 23 den Zufallszahlenerzeuger mit einem zufälligen Startwert »seed()« . Die resultierenden Zufallszahlen verteilen sich gleichmäßig auf die Zahlen von 1 bis 6 (Zeile 26). Zum Abschluss wird der virtuelle Würfel noch 60-mal in Zeile 29 geworfen, der Vektor »dice« speichert alle Ergebnisse.

Nun ist es Zeit für die Statistik: Die Aufrufe von »inRange()« in Zeile 31, 32 und 35 erzeugen die Prädikate, die das Programm sogleich anwendet. Damit ist schnell ermittelt, wie oft die 6 (Zeile 31) oder wie oft die 5 und die 6 (Zeile 32) gewürfelt wurden. Natürlich lässt sich »inRange()« auch wie in Zeile 35 einsetzen, um alle Ergebnisse mit ein bis vier Würfelaugen zu löschen.

Listing 3 verwendet Lambda-Funktionen (Zeile 7) und Closures sowie Funktionen als Objekte erster Klasse, die sich beispielsweise kopieren lassen (Zeile 14). Bei »inRange()« in Zeile 6 handelt es sich um eine Funktion höherer Ordnung, da sie eine Funktion zurückgibt. Damit bewegt sich das Beispiel tief in der funktionalen Programmierung.

Problem: Gültigkeit

Ein Problem, das der Programmierer beim Einsatz von Lambda- oder auch anonymen Funktionen im Auge behalten muss, ist die Gültigkeit der verwendeten Variablen. Gefahr ist dann im Spiel, wenn die Variablen per Referenz erfasst werden. So quittierte der Rechner des Autors das Ausführen des Programms in Listing 4 mit einem Speicherzugriffsfehler. Der Grund: Die Funktion »makeLambda()« in Zeile 4 erzeugt eine Lambda-Funktion. Diese gibt den String »val« zurück.

Listing 4

Ungültige Variable

01 #include <functional>
02 #include <iostream>
03
04 std::function<std::string()> makeLambda() {
05     const std::string val="on stack created";
06     return [&val]{return val;};
07 }
08
09 int main(){
10   auto bad= makeLambda();
11   std::cout << bad();
12 }

Kurz und knackig

Die Einsatzgebiete von Lambda-Funktionen überschneiden sich oft mit denen von Funktionen und Funktionsobjekten. Eine einfache Faustregel für die Anwendung von Lambda-Funktionen ist, ob sich deren Funktionskörper kurz und knackig niederschreiben lässt. Und das ist dann der Fall, wenn folgende Kriterien für die Lambda-Funktion zutreffen:

  • Der Funktionskörper besteht nur aus einem einfachen Ausdruck.
  • Die Lambda-Funktion wird nicht mehrmals definiert und angewandt.
  • Der Einsatz und die Funktionalität der Lambda-Funktion sind selbsterklärend, sodass keine umfangreiche Dokumentation erforderlich ist.

Das ist nichts Ungewöhnliches, doch Vorsicht: Die Lambda-Funktion erfasst »val« mit »[&val]« per Referenz. Die Gültigkeit von »val« endet aber mit der Ausführung der Funktion »makeLambda()« . Damit ist das Ergebnis des Aufrufs von »bad()« in Zeile 11 und somit das ganze Programm undefiniert. Es zeigt unvorhersagbares Verhalten [3].

Diese Problematik verschärft sich bei in Threads eingesetzten Lambda-Funktionen. Erfasst eine derartige Funktion in einem Thread eine Variable per Referenz, gilt es, neben der Gültigkeit den Schutz der Variablen zu beachten. So viel sei als Appetitanreger auf die nächste Folge verraten: In der neuen Theading-Funktionalität von C++11 werden Lambda-Funktion gerne verwendet, um die Arbeitspakete für die Threads zu schnüren.

Formsache

Ein Tipp noch zum Schluss: In Lamba-Funktionen sollte sich der Programmierer kurz fassen (siehe Kasten “Kurz und knackig”). Wer dennoch komplexere Lambda-Funktionen einsetzen möchte, sollt das gewohnte Layout einer C-Funktion verwenden. Obwohl sie unterschiedlich aussehen, bieten die beiden Lambda-Funktionen in Listing 5 die gleiche Funktionalität: Sie berechnen die Summe und das Produkt aller Elemente des Containers »val« .

Listing 5

Layout-Varianten

01 int sum= 0;
02 int prod= 1;
03 std::for_each(val.begin(),val.end(),[&](int v){sum+= v; prod*= v; std::cout << sum << " " << prod << std::endl;});
04
05 sum= 0;
06 prod= 1;
07 std::for_each(val.begin(),val.end(),
08               [&](int v){
09                 sum+= v;
10                 prod*= v;
11                 std::cout << sum << " " << prod << std::endl;
12               });

Ersetzt man in der zweiten Lambda-Funktion in Zeile 8 den Ausdruck »[&]« durch einen Funktionsnamen und den Rückgabewert »void calcSumAndProduct« , dann wird die syntaktische Verwandtschaft der Lambda- zur normalen Funktion offensichtlich. (mhu)

Compilerunterstützung für C++11

Die Compilerunterstützung der C++11-Features ist recht weit fortgeschritten, ein aktueller Compiler ist aber in jedem Fall erforderlich [4]. Der aktuelle GCC 4.6 hat bei der Umsetzung von C++11 die Nase vorn [5]. Mit ihm lassen sich nahezu alle Features der Kernsprache sowie die neuen und verbesserten Bibliotheken in Aktion beobachten. Lediglich bei der Bibliothek für die regulären Ausdrücke muss der Anwender auf Boost zurückgreifen.

Beim Compiler »g++« muss der Anwender C++11 explizit einschalten. Das bewirkt ein Aufruf in der Form »g++ -std=c++0x lambda.cpp -o lambda« . GCC 4.7 erlaubt es, den Quelltext mit »g++ -std=c++11 lambda.cpp -o lambda« zu übersetzen.

In Microsofts Visual C++ Compiler (VC10) dagegen ist der C++11-Standard von Haus aus aktiviert. Auch die Regular Expressions sind schon eingebaut [6].

Infos

  1. Rainer Grimm, “Die Elf spielt auf”: Linux-Magazin 12/11, S. 94
  2. Syntactic Sugar: http://en.wikipedia.org/wiki/Syntactic_sugar
  3. Undefiniertes Verhalten: http://en.wikipedia.org/wiki/Undefined_behavior
  4. C++11-Unterstützung: http://wiki.apache.org/stdcxx/C%2B%2B0xCompilerSupport
  5. C++11-Unterstützung für den GCC-Compiler: http://gcc.gnu.org/projects/cxx0x.html
  6. C++11-Unterstützung für den Visual C++ Compiler: http://blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx

Der Autor

Rainer Grimm arbeitet seit 1999 als Software-Entwickler bei der Science + Computing AG in Tübingen. Insbesondere hält er Schulungen für das hauseigene Produkt SC Venus. Im Dezember 2011 ist sein C++11-Buch im Verlag Addison-Wesley erschienen.

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