Lambda-Ausdrücke im Detail
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 85 von 104.
Lambda-Ausdrücke, die in C++11 eingeführt wurden, sind anonyme Funktionen, die Sie inline definieren können. Während Sie bereits grundlegende Lambdas mit STL-Algorithmen gesehen haben, schaltet das Verständnis ihrer vollständigen Syntax leistungsstarke Funktionen frei, um 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 externen Variablen das Lambda 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 als Kopie
auto allByRef = [&]() { x++; y++; }; // Alle als Referenz
auto mixed = [x, &y]() { y += x; }; // Beides gemischt
byRef();
std::cout << y << "\n"; // 25
}Standardmäßig sind per Wert erfasste Variablen innerhalb des Lambdas const. Das Schlüsselwort mutable ermöglicht die Modifikation 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 fügte Init-Captures hinzu, was es ermöglicht, neue Variablen zu erstellen oder Objekte in das Lambda zu verschieben:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Ownership wurde in das Lambda übertragenLambdas sind in der OOP besonders nützlich, wenn Sie Verhalten als Parameter übergeben müssen – für Callbacks, benutzerdefinierte Komparatoren oder Event-Handler – ohne separate Funktionsobjekte zu definieren.
Aufgabe
EinfachLassen Sie uns ein Event-Handler-System bauen, das die Leistungsfähigkeit von Lambda-Ausdrücken mit verschiedenen Capture-Modi demonstriert. Sie erstellen einen einfachen Event-Dispatcher, der Callbacks speichert und aufruft, um zu zeigen, wie Lambdas den externen Zustand auf verschiedene Weise erfassen können.
Sie organisieren Ihren Code in drei Dateien:
EventDispatcher.h: Definieren Sie eineEventDispatcher-Klasse, die Event-Callbacks verwaltet.Ihr Dispatcher sollte Callbacks mithilfe eines
std::vectorvonstd::function<void()>speichern. Fügen Sie diese Methoden hinzu:addCallback(std::function<void()> callback)— fügt der Liste einen Callback hinzufireAll()— ruft alle gespeicherten Callbacks in der richtigen Reihenfolge aufclear()— entfernt alle Callbacks
Sie müssen
<functional>und<vector>einbinden.EventDispatcher.cpp: Implementieren Sie die Methoden für Ihren Dispatcher. Die MethodefireAll()sollte einfach alle Callbacks durchlaufen und jeden einzelnen aufrufen.main.cpp: Lesen Sie zwei Eingaben ein:- Eine Basiszahl (Integer)
- Einen Multiplikator (Integer)
Erstellen Sie einen
EventDispatcherund demonstrieren Sie verschiedene Lambda-Capture-Techniken, indem Sie drei Callbacks hinzufügen:- Ein Lambda, das die Basiszahl per Wert (by value) erfasst und Folgendes ausgibt:
Base value: [base] - Ein Lambda, das den Multiplikator per Referenz (by reference) erfasst, ihn um 1 erhöht und dann ausgibt:
Multiplier after increment: [multiplier] - Ein mutable Lambda, das eine Zählervariable (initialisiert mit 0) per Wert erfasst, sie bei jedem Aufruf erhöht und ausgibt:
Call count: [counter]
Nachdem Sie alle Callbacks hinzugefügt haben, rufen Sie
fireAll()zweimal auf, um zu sehen, wie sich die verschiedenen Capture-Modi über mehrere Aufrufe hinweg verhalten. Geben Sie zwischen den beidenfireAll()-Aufrufen---als Trennzeichen aus.Geben Sie schließlich nach beiden Runden den Endwert der Multiplikator-Variable aus main aus, um zu zeigen, wie sich das Capture per Referenz darauf ausgewirkt hat:
Final multiplier: [multiplier]
Zum Beispiel mit den Eingaben 10 und 5:
Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 2
Final multiplier: 7Mit den Eingaben 42 und 0:
Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 2
Final multiplier: 2Beachten Sie die entscheidenden Verhaltensweisen: Das Capture per Wert behält die ursprüngliche Basis unverändert bei, das Capture per Referenz modifiziert die tatsächliche Multiplikator-Variable in main (akkumuliert über die Aufrufe hinweg), und der Zähler des mutable Lambdas erhöht sich bei jedem Aufruf, da fireAll() dasselbe gespeicherte Lambda-Objekt mit seiner eigenen internen Kopie aufruft, die bestehen bleibt.
Spickzettel
Lambda-Ausdrücke sind anonyme Funktionen, die inline definiert werden können. Die vollständige Syntax lautet:
[capture](parameters) mutable -> return_type { body }Capture-Klauseln
Die Capture-Klausel bestimmt, auf welche externen Variablen die Lambda-Funktion zugreifen kann und wie:
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 Variablen kopieren
auto allByRef = [&]() { x++; y++; }; // Alle Variablen als Referenz
auto mixed = [x, &y]() { y += x; }; // Beide Modi mischenMutable Lambdas
Standardmäßig sind per Wert (by-value) erfasste Variablen innerhalb der Lambda-Funktion const. Verwenden Sie mutable, um Kopien zu ändern:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // Modifiziert die Kopie der Lambda-Funktion
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - Original unverändertInit-Captures (C++14)
Erstellen Sie neue Variablen oder verschieben Sie Objekte in die Lambda-Funktion:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Besitz (Ownership) in Lambda übertragenVerwendung von Lambdas mit std::function
Speichern Sie Lambdas in std::function für Callbacks und Event-Handler:
#include <functional>
#include <vector>
std::vector<std::function<void()>> callbacks;
callbacks.push_back([x]() { /* x verwenden */ });
callbacks.push_back([&y]() { /* y modifizieren */ });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 per Wert (BY VALUE) erfasst
// Es soll Folgendes ausgeben: "Base value: [base]"
// TODO: Füge ein Lambda hinzu, das multiplier per Referenz (BY REFERENCE) erfasst
// Es soll multiplier um 1 erhöhen und dann ausgeben: "Multiplier after increment: [multiplier]"
// TODO: Füge ein MUTABLE Lambda hinzu, das einen counter (initialisiert mit 0) per Wert erfasst
// Es soll 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: Gib den finalen multiplier-Wert aus: "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 & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container12Moderne C++ Features
Move-Semantik & RvaluesPerfect ForwardingLambda-Ausdrücke im Detailstd::function & std::bindconstexpr und constevalStructured Bindingsoptional, variant, any