Lambda-Ausdrücke im Detail
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 85 von 104.
Lambda-Ausdrücke, die mit C++11 eingeführt wurden, sind anonyme Funktionen, die du inline definieren kannst. Du hast bereits grundlegende Lambdas mit STL-Algorithmen gesehen; wenn du ihre vollständige Syntax verstehst, eröffnen sich dir leistungsstarke Möglichkeiten, Variablen zu erfassen und zu steuern, wie auf sie zugegriffen wird.
Die vollständige Lambda-Syntax lautet: [capture](parameters) mutable -> return_type { body }. Die Capture-Klausel bestimmt, auf welche Variablen außerhalb des Lambdas es zugreifen kann und wie:
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // Kopie von x
auto byRef = [&y]() { y += 5; }; // Referenz auf y
auto allByValue = [=]() { return x + y; }; // Alle kopieren
auto allByRef = [&]() { x++; y++; }; // Alle referenzieren
auto mixed = [x, &y]() { y += x; }; // Beides mischen
byRef();
std::cout << y << "\n"; // 25
}Standardmäßig sind per Wert erfasste Variablen innerhalb der Lambda-Funktion const. Das Schlüsselwort mutable ermöglicht die Änderung dieser Kopien:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // Modifiziert die Kopie des Lambdas
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - Original unverändertC++14 hat Init-Captures hinzugefügt, mit denen du neue Variablen erstellen oder Objekte in die Lambda-Funktion verschieben kannst:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Besitz in die Lambda übertragenLambdas sind in der OOP besonders nützlich, wenn du Verhalten als Parameter übergeben musst – etwa für Rückruffunktionen, benutzerdefinierte Vergleichsfunktionen oder Ereignishandler –, ohne separate Funktionsobjekte zu definieren.
Aufgabe
EinfachErstellen wir ein Event-Handler-System, das die Leistungsfähigkeit von Lambda-Ausdrücken mit verschiedenen Erfassungsmodi demonstriert. Du erstellst einen einfachen Event-Dispatcher, der callbacks speichert und aufruft, und zeigst dabei, wie Lambdas externen Zustand auf verschiedene Arten erfassen können.
Du organisierst deinen Code auf drei Dateien:
EventDispatcher.h: Definiere eineEventDispatcher-Klasse, die Event-callbacks verwaltet.Dein Dispatcher sollte callbacks mithilfe eines
std::vectorausstd::function<void()>speichern. Füge diese Methoden ein:addCallback(std::function<void()> callback): fügt der list einen callback hinzufireAll(): ruft alle gespeicherten callbacks der Reihe nach aufclear(): entfernt alle callbacks
Du musst
<functional>und<vector>einbinden.EventDispatcher.cpp: Implementiere die Methoden für deinen Dispatcher. Die MethodefireAll()sollte einfach durch alle callbacks iterieren und jeden einzelnen aufrufen.main.cpp: Lies zwei Eingaben ein:- Eine Basiszahl (Ganzzahl)
- Einen multiplier (Ganzzahl)
Erstelle einen
EventDispatcherund demonstriere verschiedene Lambda-Erfassungstechniken, indem du drei callbacks hinzufügst:- Ein Lambda, das die Basiszahl per Wert erfasst und ausgibt:
Base value: [base] - Ein Lambda, das den multiplier per Referenz erfasst, ihn um 1 erhöht und anschließend ausgibt:
Multiplier after increment: [multiplier] - Ein mutable Lambda, das eine counter-Variable (mit dem Anfangswert 0) per Wert erfasst, sie bei jedem Aufruf erhöht und ausgibt:
Call count: [counter]
Rufe nach dem Hinzufügen aller callbacks
fireAll()zweimal auf, um zu sehen, wie sich die verschiedenen Erfassungsmodi bei mehreren Aufrufen verhalten. Gib zwischen den beidenfireAll()-Aufrufen---als Trennzeichen aus.Gib schließlich nach beiden Durchläufen den endgültigen Wert der multiplier-Variable aus
mainaus, um zu zeigen, wie sich die Erfassung per Referenz auf sie ausgewirkt hat:Final multiplier: [multiplier]
Beispiel: Bei den Eingaben 10 und 5:
Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7Bei den Eingaben 42 und 0:
Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2Beachte die wichtigsten Verhaltensweisen: Die Erfassung per Wert lässt die ursprüngliche Basis unverändert, die Erfassung per Referenz verändert die tatsächliche multiplier-Variable in main (und sammelt die Änderungen über mehrere Aufrufe hinweg), und der counter des mutable Lambdas liest in jedem Durchlauf 1. Das liegt daran, dass fireAll() seine callbacks per Wert durchläuft: In jedem Durchlauf wird eine neue Kopie der gespeicherten std::function aufgerufen, deren erfasster counter bei 0 beginnt und auf 1 erhöht wird. Daher wird der eigene Zustand des gespeicherten callbacks nie weiterentwickelt.
Probier es selbst
#include <iostream>
#include "EventDispatcher.h"
using namespace std;
int main() {
int base;
int multiplier;
cin >> base;
cin >> multiplier;
EventDispatcher dispatcher;
// TODO: Füge ein Lambda hinzu, das base BY VALUE erfasst
// Es sollte ausgeben: "Base value: [base]"
// TODO: Füge ein Lambda hinzu, das multiplier BY REFERENCE erfasst
// It should increment multiplier by 1, then print: "Multiplier after increment: [multiplier]"
// TODO: Füge ein MUTABLE Lambda hinzu, das einen counter (initialisiert mit 0) per Wert erfasst
// Es sollte den counter erhöhen und ausgeben: "Call count: [counter]"
// TODO: Rufe fireAll() auf, um alle Callbacks aufzurufen
// TODO: Gib "---" als Trennzeichen aus
// TODO: Rufe fireAll() erneut auf
// TODO: Print the final multiplier value: "Final multiplier: [multiplier]"
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer Container12Moderne C++-Funktionen
Move-Semantik & RvaluesPerfektes ForwardingLambda-Ausdrücke im Detailstd::function & std::bindconstexpr und constevalStrukturierte Bindungenoptional, variant, anyÜbe selbstständig: Online-C++-Compiler