================ Lambda-Ausdrücke ================ Jedesmal eine vollständige Klassendeklaration aufzuschreiben, wenn nur eine einzeilige Funktion mit etwas Kontext übergeben werden soll, erscheint aufwendig. Auch tragen solche Konstrukte eher zur Unübersichtlichkeit des Programmtexts bei. Es gibt in anderen Programmiersprachen schon sehr lange ein Konstrukt für anonyme Funktionen, bei denen - die Funktion selbst keinen Namen tragen muss (daher anonym), - die Funktion innerhalb eines Ausdrucks konstruiert wird und - diese Funktion lokale Variablen aus ihrer Umgebung verwenden darf, d.h. der Kontext (_closure_ genannt) steht automatisch zur Verfügung. Solche anonymen Funktionen bzw. Ausdrücke, die eine Funktion erzeugen, werden Lambda-Ausdrücke genannt. Dies geht zurück auf das Lambda-Kalkül von Alonzo Church und Stephen Kleene, die in den 30er-Jahren damit ein formales System für berechenbare Funktionen entwickelten. Zu den wichtigsten Arbeiten aus dieser Zeit gehört der Aufsatz von Alonzo Church: _An Unsolvable Problem of Elementary Number Theory_. _American Journal of Mathematics_, Band 58, Nr. 2 (April 1936), S. 345-363. Im Netzwerk der Universität Ulm __abrufbar__. :links: abrufbar -> http://www.jstor.org/stable/2371045 Bei den Programmiersprachen wurden diese Ausdrücke zuerst von Lisp eingeführt und anschließend auch von anderen Sprachen übernommen. Ein Nachteil der Lambda-Ausdrücke ist der Implementierungsaufwand, da lokale Variablen, die normalerweise effizient auf dem Stack untergebracht werden, dank eines Lambda-Ausdrucks unter Umständen länger überleben müssen. Das wird normalerweise gelöst, indem die Variablen aus der _closure_ auf dem Heap angelegt werden und mit Hilfe der _garbage collection_ später wieder automatisiert freigegeben werden. Diese Vorgehensweise erschien für eine Programmiersprache wie C++ so unattraktiv, dass erst sehr spät der Gedanke kam, hier eine Lösung zu finden. Die prinzipielle Idee in C++ sieht so aus: - Lambda-Ausdrücke führen zum impliziten Anlegen einer anonymen Klasse (analog zu `RandomValues`) mit einem Funktionsoperator. - Es muss genau spezifiziert werden, welche Variablen aus der Umgebung (_closure_) wie für die anonyme Funktion zur Verfügung gestellt werden. Hier werden zwei Varianten unterstützt: * Die Variablen werden kopiert. * Die Variablen werden per Referenz übernommen. Diese Variablen werden entsprechend in die implizit erzeugte Klasse übernommen und automatisch vom Konstruktor berücksichtigt. Das bedeutet, dass die Lebenszeit lokaler Variablen sich nicht verändert, wenn sie zur _closure_ eines Lambda-Ausdrucks gehören. Wenn mit Referenzen gearbeitet wird, liegt es in der Verantwortung des Programmierers darauf zu achten, dass der Lambda-Ausdruck nicht länger benutzt wird als die lokalen Variablen leben. So ließe sich die Funktion `init_value` durch einen Lambda-Ausdruck ersetzen: ---- CODE (type=cpp) ---------------------------------------------------------- GeMatrix A(4, 5, StorageOrder::ColMajor); initGeMatrix(A, [&](std::size_t i, std::size_t j) -> double { return j * A.n + i + 1; }); ------------------------------------------------------------------------------- Ein Lambda-Ausdruck beginnt in C++ immer mit der _capture_, d.h. einem in `[...]` gefassten Konstrukt, das angibt, wie lokale Variablen aus der Umgebung zu übernehmen sind. Hier bedeutet `[&]` konkret, dass alle benötigten Variablen per Referenz übernommen werden. Das betrifft hier konkret nur `A`, das wir nicht unbeabsichtigt kopieren wollen. Nach der _capture_ folgt die Parameterliste in gewohnter Weise. Der Return-Typ des Lambda-Ausdrucks darf erst hinter der Parameterliste angegeben werden. Vor dem Return-Typ ist hierbei `->` anzugeben. Danach folgt der Anweisungsblock der anonymen Funktion, die jetzt alle Parameter und auch die von der _capture_ erfassten lokalen Variablen verwenden darf. Der Übersetzer erzeugt für diesen Lambda-Ausdruck implizit eine anonyme Klasse, die in etwa wie folgt aussieht: ---- CODE (type=cpp) ---------------------------------------------------------- struct Anonymous { GeMatrix& A; Anonymous(GeMatrix& A) : A(A) { } double operator(std::size_t i, std::size_t j) const { return j * A.n + i + 1; } }; ------------------------------------------------------------------------------- Bemerkenswert ist hier, dass der Funktions-Operator implizit mit `const` deklariert wird und somit die Matrix `A` nicht verändern darf. Da bei der _capture_ `[&]` angegeben worden ist, wurde hier `A` per Referenz übergeben und nur eine Referenz behalten. Dort wo der Lambda-Ausdruck steht, erzeugt dann der Übersetzer ein temporäres Objekt der anonymen Klasse: ---- CODE (type=cpp) ---------------------------------------------------------- GeMatrix A(4, 5, StorageOrder::ColMajor); initGeMatrix(A, Anonymous(A)); ------------------------------------------------------------------------------- Daraus lässt sich auch erkennen, dass Lambda-Ausdrücke temporäre Funktionsobjekte erzeugen. Analog können wir auch herangehen, die Initialisierung mit Pseudo-Zufallszahlen einem Lambda-Ausdruck zu überlassen. Allerdings wäre ein `const`-Funktionsoperator nicht hilfreich, da wir den Status des Pseudo-Zufallszahlengenerators bei jedem Abruf verändern. Deswegen muss hier `mutable` angegeben werden. Die anonyme Funktion benötigt hier `mt` und `uniform` aus der Umgebung. Diese könnten wir hier auch per Referenz übernehmen. In diesem Fall erscheint aber das Kopieren einfacher: ---- CODE (type=cpp) ---------------------------------------------------------- GeMatrix B(3, 7, StorageOrder::ColMajor); { std::random_device random; std::mt19937 mt(random()); std::uniform_real_distribution uniform(-100, 100); initGeMatrix(B, [=](std::size_t i, std::size_t j) mutable -> double { return uniform(mt); }); } ------------------------------------------------------------------------------- Aufgaben ======== * Ersetzen Sie die Klasse `IncreasingRandomValues` durch einen Lambda-Ausdruck. Wenn Sie in Ihrer _closure_ einige Variablen per Kopie, andere per Referenz übernehmen wollen, können sie beispielsweise mit ---- CODE (type=cpp) ------------------------------------------------------- [=,&var] /* ... */ ---------------------------------------------------------------------------- alle Variablen als Kopie übernehmen mit Ausnahme von `var`, das per Referenz übernommen wird. * Entwickeln Sie eine verallgemeinerte Fassung von `initGeMatrix` mit dem Namen `applyGeMatrix`, die durch sämtliche Werte einer Matrix iteriert und auf jedes Element ein als Parameter übergebenes Funktionsobjekt zusammen mit den Indizes aufruft (d.h. ein Lambda-Ausdruck hätte dann drei Parameter). Zeigen Sie, wie mit Hilfe von `applyGeMatrix` - eine Initialisierung durchgeführt werden kann, - sich die Summe aller Elemente berechnen lässt und - der Betrag des betragsgrößten Elements ermittelt werden kann. * Bislang wurde `asumDiffGeMatrix` verwendet, um die Summe der Beträge der Differenzen zu berechnen. Es bietet sich hier an, - eine Klasse `GeMatrixCombinedConstView` zu erstellen, die eine virtuelle Sicht $C$ nur zum Lesen auf zwei gleichdimensionierte Matrizen $A$ und $B$ liefert mit $C_{i,j} = A_{i,j} \odot B_{i,j}$, wobei der Operator $\odot$ durch ein Funktionsobjekt mit zwei Parametern spezifiziert werden kann, - diese Klasse mit einem Operator zu verwenden, der für die Operanden $a$ und $b$ den Betrag der Differenz $\mid a - b \mid$ zurückliefert, und - dann mit Hilfe von `applyGeMatrix` wie in der vorherigen Aufgabe die Summe aller Elemente dieser kombinierten Sicht berechnet wird. Sie dürfen hierzu gerne vereinfacht die Annahme treffen, dass beide Matrizen den gleichen Typ haben. Wenn Sie darauf verzichten, empfiehlt sich die Verwendung von `std::common_type` aus ``. Ein größeres Problem ist die Frage, wie das Funktionsobjekt in der Klasse `GeMatrixCombinedConstView` aufbewahrt werden kann. Der Typ ist nicht bekannt und diesen als Template-Parameter für die Klasse zu integrieren, hat den Nachteil, dass die Instantiierung nicht ohne eine Hilfs-Template-Funktion möglich ist. Ein Ausweg besteht darin, den Konstruktor selbst als Template-Funktion auszuführen. Dann kann aber endgültig kein Datentyp mehr für das Funktionsobjekt als Variable angegeben werden. Hier kommt Hilfe durch die Template-Klasse `std::function` aus ``, die beliebige Funktionsobjekte aufnehmen kann. Parametrisiert wird `std::function` mit dem entsprechenden Funktionstyp des Funktionsoperators. So kann etwa der Datentyp `std::function` verwendet werden, um ein beliebiges Funktionsobjekt zu beherbigen, dass zwei Parameter des Typs `ElementType` akzeptiert und ein Wert des Typs `ElementType` zurückgibt. Vorlage ======= :import: session11/step05/bench.h :import: session11/step05/gematrix.h :import: session11/step05/test_initmatrix.cpp :navigate: up -> doc:index back -> doc:session11/page04 next -> doc:session11/page06