Open Source im professionellen Einsatz
Linux-Magazin 02/2012

Modernes C++ in der Praxis – Folge 2

Kurz und knackig

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.

1157

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.

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).

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.

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.

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • C++11

    2014 ist ein besonderes Jahr für C++. Drei Jahre nach C++11 erfährt der Sprachstandard mit C++14 den letzten Feinschliff. Neben generischen Lambda-Funktionen und der vereinfachten Ermittlung des Rückgabetyps kann C++14 vor allem mit einem Feature punkten: Reader-Writer-Locks.

  • C++11

    Container sind nicht nur bei der Virtualisierung derzeit in aller Munde, auch dem Programmierer sind Behälter für Objekte nützlich. Die Version 11 von C++ enthält einige Algorithmen, die die Arbeit mit solchen Containern deutlich vereinfachen.

  • C++11

    Die jüngste Reform der Programmiersprache C++ ist abgeschlossen. Das Linux-Magazin widmet dem neuen Standard C++11 eine Artikelserie, die dem Programmierer zeigt, wie er in der Praxis von den Neuerungen profitiert. Die erste Folge stellt Lambda-Funktionen vor.

  • C++

    Beim deklarativen Programmieren drückt der C++-Programmierer unter anderem mit Hilfe von Schlüsselwörtern aus, was er erreichen möchte. Der Compiler kümmert sich dann um den weiteren Weg.

  • C++11

    C++11 kennt zwei neue Literale: Raw-String- und benutzerdefinierte Literale. Während Raw-String-Literale den Interpreter zum Beispiel davon abbringen, Backslashes in Zeichenketten zu interpretieren, schützen benutzerdefinierte Literale dereinst womöglich Nasa-Sonden vor dem Verglühen.

comments powered by Disqus

Ausgabe 06/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.