Guten Tag, Freunde. Heute haben wir für Sie eine Übersetzung des ersten Teils des Artikels
„Lambdas: von C ++ 11 nach C ++ 20“ vorbereitet. Die Veröffentlichung dieses Materials fällt zeitlich mit dem Start des Kurses
"C ++ Developer" zusammen , der morgen beginnt.
Lambda-Ausdrücke sind eine der leistungsstärksten Ergänzungen in C ++ 11 und werden mit jedem neuen Sprachstandard weiterentwickelt. In diesem Artikel gehen wir auf ihre Geschichte ein und betrachten die Entwicklung dieses wichtigen Teils des modernen C ++.

Der zweite Teil ist hier verfügbar:
Lambdas: Von C ++ 11 bis C ++ 20, Teil 2EintragBei einem lokalen C ++ - Benutzergruppentreffen hatten wir eine Live-Programmiersitzung zur „Geschichte“ von Lambda-Ausdrücken. Das Gespräch wurde vom C ++ - Experten Tomasz Kamiński geführt (
siehe Thomas 'Linkedin-Profil ). Hier ist die Veranstaltung:
Lambdas: Von C ++ 11 bis C ++ 20 - C ++ User Group KrakowIch beschloss, den Code von Thomas zu nehmen (mit seiner Erlaubnis!), Ihn zu beschreiben und einen separaten Artikel zu erstellen.
Wir beginnen mit der Untersuchung von C ++ 03 und der Notwendigkeit kompakter lokaler Funktionsausdrücke. Dann fahren wir mit C ++ 11 und C ++ 14 fort. Im zweiten Teil der Serie werden wir Änderungen in C ++ 17 sehen und sogar einen Blick darauf werfen, was in C ++ 20 passieren wird.
Lambdas in C ++ 03Von Anfang an konnten die
std::algorithms
STL, wie z. B.
std::sort
, jedes aufgerufene Objekt für Containerelemente aufrufen. In C ++ 03 umfasste dies jedoch nur Zeiger auf Funktionen und Funktoren.
Zum Beispiel:
#include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Laufender Code:
@WandboxDas Problem war jedoch, dass Sie eine separate Funktion oder einen Funktor in einem anderen Bereich und nicht im Bereich des Algorithmusaufrufs schreiben mussten.
Als mögliche Lösung können Sie eine lokale Funktorklasse schreiben, da C ++ diese Syntax immer unterstützt. Aber es funktioniert nicht ...
Schauen Sie sich diesen Code an:
int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Versuchen Sie es mit
-std=c++98
kompilieren und Sie werden den folgenden Fehler in GCC sehen:
error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor'
Im Wesentlichen können Sie in C ++ 98/03 keine Instanz einer Vorlage mit einem lokalen Typ erstellen.
Aufgrund all dieser Einschränkungen begann das Komitee mit der Entwicklung einer neuen Funktion, die wir erstellen und als "an Ort und Stelle" bezeichnen können ... "Lambda-Ausdrücke"!
Wenn wir uns
N3337 - die endgültige Version von C ++ 11 - ansehen, sehen wir einen separaten Abschnitt für Lambdas:
[expr.prim.lambda] .
Neben C ++ 11Ich denke, Lambdas wurden der Sprache mit Bedacht hinzugefügt. Sie verwenden die neue Syntax, aber der Compiler "erweitert" sie auf eine echte Klasse. Somit haben wir alle Vor- (und manchmal auch Nachteile) einer wirklich streng typisierten Sprache.
Hier ist ein grundlegendes Codebeispiel, das auch das entsprechende lokale Funktorobjekt zeigt:
#include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); }
Beispiel:
@WandBoxSie können sich auch CppInsights ansehen, das zeigt, wie der Compiler den Code erweitert:
Schauen Sie sich dieses Beispiel an:
CppInsighs: Lambda-TestIn diesem Beispiel konvertiert der Compiler:
[] (int x) { std::cout << x << '\n'; }
In etwas Ähnliches (vereinfachte Form):
struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance;
Lambda-Ausdruckssyntax:
[] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | | |
Einige Definitionen, bevor wir beginnen:
Aus
[expr.prim.lambda # 2] :
Die Auswertung eines Lambda-Ausdrucks führt zu einem temporären Wert. Dieses temporäre Objekt wird als
Abschlussobjekt bezeichnet .
Und aus
[expr.prim.lambda # 3] :
Der Typ des Lambda-Ausdrucks (der auch der Typ eines Abschlussobjekts ist) ist ein eindeutiger namenloser Nicht-Vereinigungstyp der Klasse, der als
Abschluss-Typ bezeichnet wird .
Einige Beispiele für Lambda-Ausdrücke:
Zum Beispiel:
[](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; }
Lambda-TypDa der Compiler für jedes Lambda einen eindeutigen Namen generiert, ist es nicht möglich, diesen im Voraus zu kennen.
auto myLambda = [](int a) -> double { return 2.0 * a; }
Außerdem
[expr.prim.lambda] :
Der dem Lambda-Ausdruck zugeordnete Schließungstyp verfügt über einen Remote-Standardkonstruktor ([dcl.fct.def.delete]) und einen Remote-Zuweisungsoperator.
Daher können Sie nicht schreiben:
auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy;
Dies führt zu folgendem Fehler in GCC:
error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor
Betreiber anrufenDer Code, den Sie in den Lambda-Körper eingeben, wird in den operator () -Code des entsprechenden Verschlusstyps "übersetzt".
Standardmäßig ist dies eine integrierte Konstantenmethode. Sie können es ändern, indem Sie mutable angeben, nachdem Sie die Parameter deklariert haben:
auto myLambda = [](int a) mutable { std::cout << a; }
Obwohl die Konstantenmethode für ein Lambda ohne leere Erfassungsliste kein „Problem“ ist, ist es wichtig, wann Sie etwas erfassen möchten.
Erfassen[] führt nicht nur ein Lambda ein, sondern enthält auch eine Liste der erfassten Variablen. Dies wird als Erfassungsliste bezeichnet.
Durch Erfassen einer Variablen erstellen Sie ein Kopierelement dieser Variablen im Schließungstyp. Dann können Sie innerhalb des Lambda-Körpers darauf zugreifen.
Die grundlegende Syntax lautet:
- [&] - Erfassung als Referenz, alle Variablen im automatischen Speicher werden im Gültigkeitsbereich deklariert
- [=] - Nach Wert erfassen, der Wert wird kopiert
- [x, & y] - erfasst x explizit nach Wert und y nach Referenz
Zum Beispiel:
int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; }
Sie können hier mit dem vollständigen Beispiel
herumspielen :
@WandboxObwohl die Angabe von
[=]
oder
[&]
praktisch sein kann - da alle Variablen im automatischen Speicher erfasst werden, ist es offensichtlicher, Variablen explizit zu erfassen. So kann der Compiler Sie vor unerwünschten Effekten warnen (siehe z. B. Hinweise zu globalen und statischen Variablen).
Weitere Informationen finden Sie in Abschnitt 31 von Effective Modern C ++ von Scott Meyers: „Vermeiden Sie die Standarderfassungsmodi.“
Und ein wichtiges Zitat:
C ++ - Schließungen verlängern nicht die Lebensdauer erfasster Links.
VeränderlichStandardmäßig ist der Operator für den Schließungstyp () konstant, und Sie können die erfassten Variablen im Hauptteil eines Lambda-Ausdrucks nicht ändern.
Wenn Sie dieses Verhalten ändern möchten, müssen Sie das veränderbare Schlüsselwort nach der Parameterliste hinzufügen:
int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl;
Im obigen Beispiel können wir die Werte von x und y ändern ... dies sind jedoch nur Kopien von x und y aus dem angehängten Bereich.
Globale VariablenerfassungWenn Sie einen globalen Wert haben und dann [=] in einem Lambda verwenden, denken Sie möglicherweise, dass der globale Wert auch vom Wert erfasst wird ... aber nicht.
int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); }
Sie können hier mit dem Code spielen:
@Wandbox
Es werden nur Variablen im automatischen Speicher erfasst. GCC kann sogar die folgende Warnung ausgeben:
warning: capture of variable 'global' with non-automatic storage duration
Diese Warnung wird nur angezeigt, wenn Sie die globale Variable explizit erfassen. Wenn Sie also
[=]
, hilft Ihnen der Compiler nicht weiter.
Der Clang-Compiler ist nützlicher, da er einen Fehler generiert:
error: 'global' cannot be captured because it does not have automatic storage duration
Siehe
@WandboxErfassen statischer VariablenDas Erfassen statischer Variablen ähnelt dem Erfassen globaler Variablen:
#include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); }
Sie können hier mit dem Code spielen:
@WandboxFazit:
10 11 12
Auch hier wird eine Warnung nur angezeigt, wenn Sie eine statische Variable explizit erfassen. Wenn Sie also
[=]
, hilft Ihnen der Compiler nicht weiter.
Erfassung von KlassenmitgliedernWissen Sie, was nach der Ausführung des folgenden Codes passiert:
#include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
Der Code deklariert ein Baz-Objekt und ruft dann
foo()
. Beachten Sie, dass
foo()
ein Lambda (gespeichert in
std::function
foo()
zurückgibt, das ein Mitglied der Klasse erfasst.
Da wir temporäre Objekte verwenden, können wir nicht sicher sein, was passieren wird, wenn f1 und f2 aufgerufen werden. Dies ist ein Problem mit baumelnden Verbindungen, das undefiniertes Verhalten verursacht.
Ähnlich:
struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo();
Spielen Sie mit dem
@ Boxbox- Code
Nochmals, wenn Sie die Erfassung explizit angeben ([s]):
std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; }
Der Compiler verhindert Ihren Fehler:
In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ...
Siehe ein Beispiel:
@WandboxNur bewegliche ObjekteWenn Sie ein Objekt haben, das nur verschoben werden kann (z. B. unique_ptr), können Sie es nicht als erfasste Variable in ein Lambda einfügen. Die Erfassung nach Wert funktioniert nicht, daher können Sie nur nach Referenz erfassen. Dies überträgt sie jedoch nicht an Sie, und wahrscheinlich ist dies nicht das, was Sie wollten.
std::unique_ptr<int> p(new int[10]); auto foo = [p] () {};
Konstanten speichernWenn Sie eine konstante Variable erfassen, bleibt die Konstanz erhalten:
int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo();
Siehe Code:
@WandboxRückgabetypIn C ++ 11 können Sie das
trailing
Lambda-Rückgabetyps überspringen, und der Compiler gibt ihn dann für Sie aus.
Anfangs war die Ausgabe des Rückgabewerttyps auf Lambdas beschränkt, die eine Rückgabeanweisung enthielten. Diese Einschränkung wurde jedoch schnell aufgehoben, da bei der Implementierung einer bequemeren Version keine Probleme auftraten.
Siehe
C ++ Standard Core Language-Fehlerberichte und akzeptierte Probleme (danke an Thomas für das Finden des richtigen Links!)
Ab C ++ 11 kann der Compiler daher auf den Typ des Rückgabewerts schließen, wenn alle Rückgabeanweisungen in denselben Typ konvertiert werden können.
Wenn alle return-Anweisungen nach der Konvertierung lvalue-to-rvalue (7.1 [conv.lval]), array-to-pointer (7.2 [conv.array]) und function-to-pointer (7.3 [conv. func]) ist der gleiche wie der generische Typ;
auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; };
Sie können hier mit dem Code spielen:
@WandboxDas obige Lambda enthält zwei
return
, die jedoch alle auf
double
verweisen, sodass der Compiler auf den Typ schließen kann.
IIFE - Sofort aufgerufener FunktionsausdruckIn unseren Beispielen habe ich ein Lambda definiert und es dann mit dem Closure-Objekt aufgerufen ... aber es kann auch sofort aufgerufen werden:
int x = 1, y = 1; [&]() { ++x; ++y; }();
Ein solcher Ausdruck kann bei der komplexen Initialisierung konstanter Objekte nützlich sein.
const auto val = []() { }();
Ich habe mehr darüber im
Beitrag IIFE for Complex Initialization geschrieben .
In einen Funktionszeiger konvertierenDer Schließungstyp für einen Lambda-Ausdruck ohne Erfassung verfügt über eine offene nicht virtuelle implizite Funktion zum Konvertieren einer Konstante in einen Zeiger auf eine Funktion, die dieselben Parameter- und Rückgabetypen wie der Operator zum Aufrufen einer Funktion des Schließungstyps hat. Der von dieser Konvertierungsfunktion zurückgegebene Wert muss die Adresse der Funktion sein, die beim Aufruf den gleichen Effekt hat wie der Aufruf des Operators einer Funktion eines Typs, der einem Schließungstyp ähnlich ist.
Mit anderen Worten, Sie können Lambdas ohne Captures in einen Funktionszeiger konvertieren.
Zum Beispiel:
#include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); }
Sie können hier mit dem Code spielen:
@WandboxVerbesserungen in C ++ 14N4140 Standard und Lambda:
[expr.prim.lambda] .
C ++ 14 hat zwei wesentliche Verbesserungen an Lambda-Ausdrücken hinzugefügt:
- Erfasst mit dem Initializer
- Gemeinsame Lambdas
Diese Funktionen lösen mehrere Probleme, die in C ++ 11 sichtbar waren.
RückgabetypDie Ausgabe des Rückgabewerttyps des Lambda-Ausdrucks wurde aktualisiert, um den automatischen Ausgaberegeln für Funktionen zu entsprechen.
[expr.prim.lambda # 4]Der Rückgabetyp des Lambda ist auto und wird durch den nachfolgenden Rückgabetyp ersetzt, sofern er bereitgestellt und / oder aus den Rückgabeanweisungen abgeleitet wird, wie in [dcl.spec.auto] beschrieben.
Erfasst mit dem InitializerKurz gesagt, wir können eine neue Elementvariable des Verschlusstyps erstellen und sie dann im Lambda-Ausdruck verwenden.
Zum Beispiel:
int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); }
Dies kann verschiedene Probleme lösen, beispielsweise bei Typen, die nur zum Verschieben verfügbar sind.
UmzugJetzt können wir das Objekt auf ein Element des Verschlusstyps verschieben:
#include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; }
OptimierungEine andere Idee ist es, es als mögliche Optimierungstechnik zu verwenden. Anstatt jedes Mal, wenn wir das Lambda aufrufen, einen Wert zu berechnen, können wir ihn einmal im Initialisierer berechnen:
#include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); }
Erfassen Sie eine MitgliedsvariableEin Initialisierer kann auch zum Erfassen einer Elementvariablen verwendet werden. Dann können wir eine Kopie der Mitgliedsvariablen erhalten und müssen uns keine Gedanken über baumelnde Links machen.
Zum Beispiel:
struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
Sie können hier mit dem Code spielen:
@Wandbox
In
foo()
erfassen wir eine Mitgliedsvariable, indem wir sie in den Schließungstyp kopieren. Darüber hinaus verwenden wir auto, um die gesamte Methode auszugeben (zuvor konnten wir in C ++ 11 die
std::function
).
Generische Lambda-AusdrückeEine weitere signifikante Verbesserung ist das generalisierte Lambda.
Ab C ++ 14 können Sie schreiben:
auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world");
Dies entspricht der Verwendung einer Vorlagendeklaration in einer Aufrufanweisung des Schließungstyps:
struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance;
Ein solches verallgemeinertes Lambda kann sehr nützlich sein, wenn es schwierig ist, einen Typ abzuleiten.
Zum Beispiel:
std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } };
Liege ich hier falsch Hat der Eintrag den richtigen Typ?
.
.
.
Wahrscheinlich nicht, da der Werttyp für std :: map
std::pair<const Key, T>
. Mein Code erstellt also zusätzliche Kopien der Zeilen ...
Dies kann mit
auto
behoben werden:
std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } );
Sie können hier mit dem Code spielen:
@WandboxFazitWas für eine Geschichte!
In diesem Artikel haben wir mit den ersten Tagen der Lambda-Ausdrücke in C ++ 03 und C ++ 11 begonnen und sind zu einer verbesserten Version in C ++ 14 übergegangen.
Sie haben gesehen, wie man ein Lambda erstellt, wie die Grundstruktur dieses Ausdrucks aussieht, was eine Erfassungsliste ist und vieles mehr.
Im nächsten Teil des Artikels werden wir zu C ++ 17 übergehen und die zukünftigen Funktionen von C ++ 20 kennenlernen.
Der zweite Teil ist hier verfügbar:
Lambdas: Von C ++ 11 bis C ++ 20, Teil 2
Referenzen
C ++ 11 -
[expr.prim.lambda]C ++ 14 -
[expr.prim.lambda]Lambda-Ausdrücke in C ++ | Microsoft-DokumenteEntmystifizierung von C ++ - Lambdas - Sticky Bits - Powered by Feabhas; Sticky Bits - Powered by Feabhas
Wir warten auf Ihre Kommentare und laden alle Interessierten zum Kurs
"C ++ Developer" ein .