Lambdas: von C ++ 11 bis C ++ 20. Teil 2

Hallo Habrowsk. Im Zusammenhang mit dem Beginn der Rekrutierung in einer neuen Gruppe im Kurs „C ++ Developer“ teilen wir Ihnen die Übersetzung des zweiten Teils des Artikels „Lambdas: von C ++ 11 nach C ++ 20“ mit. Der erste Teil kann hier gelesen werden .



Im ersten Teil der Serie haben wir Lambdas in Bezug auf C ++ 03, C ++ 11 und C ++ 14 betrachtet. In diesem Artikel habe ich die Gründe für diese leistungsstarke C ++ - Funktion, die grundlegende Verwendung, die Syntax und die Verbesserungen der einzelnen Sprachstandards beschrieben. Ich habe auch einige Grenzfälle erwähnt.
Jetzt ist es Zeit, zu C ++ 17 überzugehen und einen Blick in die Zukunft zu werfen (ganz nah!): C ++ 20.

Eintrag

Eine kleine Erinnerung: Die Idee für diese Serie kam nach einem unserer letzten C ++ User Group-Treffen in Krakau.

Wir hatten eine Live-Programmiersitzung über die „Geschichte“ der Lambda-Ausdrücke. Das Gespräch wurde vom C ++ - Experten Thomas Kaminsky geführt ( siehe Linkedin-Profil von Thomas ). Hier ist die Veranstaltung:
Lambdas: Von C ++ 11 bis C ++ 20 - C ++ User Group Krakow .

Ich beschloss, den Code von Thomas zu nehmen (mit seiner Erlaubnis!) Und darauf basierende Artikel zu schreiben. Im ersten Teil der Serie sprach ich über die Lambda-Ausdrücke wie folgt:

  • Grundlegende Syntax
  • Lambda-Typ
  • Betreiber anrufen
  • Erfassen von Variablen (veränderbare, globale, statische Variablen, Klassenmitglieder und dieser Zeiger, nur verschiebbare Objekte, Speichern von Konstanten):

    • Rückgabetyp
    • IIFE - Sofort aufgerufener Funktionsausdruck
    • Umwandlung in einen Funktionszeiger
    • Rückgabetyp
    • IIFE - Sofort aufgerufene Ausdrücke
    • In einen Funktionszeiger konvertieren
  • Verbesserungen in C ++ 14

    • Ausgabe vom Typ Rückgabe
    • Mit Initialisierer aufnehmen
    • Erfassen Sie eine Mitgliedsvariable
    • Generische Lambda-Ausdrücke

Die obige Liste ist nur ein Teil der Geschichte der Lambda-Ausdrücke!

Nun wollen wir sehen, was sich in C ++ 17 geändert hat und was wir in C ++ 20 bekommen!

Verbesserungen in C ++ 17

Standard (Entwurf vor Veröffentlichung) N659 Abschnitt über Lambdas: [expr.prim.lambda] . C ++ 17 brachte zwei signifikante Verbesserungen für Lambda-Ausdrücke:

  • constexpr lambda
  • Erfassen Sie * dies

Was bedeuten diese Innovationen für uns? Lass es uns herausfinden.

constexpr Lambda-Ausdrücke

Ab C ++ 17 definiert der Standard den operator() für einen Lambda-Typ implizit als constexpr , wenn möglich:
Aus expr.prim.lambda # 4 :
Der Funktionsaufrufoperator ist eine constexpr-Funktion, wenn auf die Deklaration des Bedingungsparameters des entsprechenden Lambda-Ausdrucks constexpr folgt oder er die Anforderungen für die constexpr-Funktion erfüllt.

Zum Beispiel:

 constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4); 

Denken Sie daran, dass in C ++ 17 constexpr Funktion die folgenden Regeln befolgen muss:

  • es sollte nicht virtuell sein;

    • Der Rückgabetyp muss ein Literaltyp sein.
    • Jeder der Typen seiner Parameter muss ein Literaltyp sein.
    • Sein Hauptteil muss = delete, = default oder eine zusammengesetzte Anweisung sein, die nicht enthält
      • ASM-Definitionen
      • gehe zu Ausdrücken,
      • Tags
      • versuche block oder
      • die Definition einer nicht-literalen Variablen, einer statischen Variablen oder einer Streaming-Speichervariablen, für die keine Initialisierung durchgeführt wird.

Was ist mit einem praktischeren Beispiel?

 template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); } 

Sie können hier mit dem Code spielen: @Wandbox

Der Code verwendet constexpr lambda und wird dann an den einfachen SimpleAccumulate Algorithmus übergeben. Der Algorithmus verwendet mehrere C ++ 17-Elemente: Die constexpr Ergänzungen zu std::array , std::begin und std::end (in einer for Schleife mit einem Bereich verwendet) sind jetzt auch constexpr , sodass der gesamte Code ausgeführt werden kann zur Kompilierungszeit.

Das ist natürlich nicht alles.

Sie können Variablen erfassen (vorausgesetzt, sie sind auch constexpr ):

 constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); } 

Es gibt jedoch einen interessanten Fall, in dem Sie die erfasste Variable nicht weitergeben, zum Beispiel:

 constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x }; 

In diesem Fall können wir in Clang die folgende Warnung erhalten:

warning: lambda capture 'x' is not required to be captured for this use

Dies liegt wahrscheinlich daran, dass x bei jeder Verwendung geändert werden kann (es sei denn, Sie übertragen es weiter oder nehmen die Adresse dieses Namens).

Aber bitte sagen Sie mir, ob Sie die offiziellen Regeln für dieses Verhalten kennen. Ich habe nur gefunden (von cppreference ) (aber ich kann es nicht im Entwurf finden ...)

(Anmerkung des Übersetzers: Während unsere Leser schreiben, meine ich wahrscheinlich, den Wert von 'x' an jeder Stelle zu ersetzen, an der er verwendet wird. Es ist definitiv unmöglich, ihn zu ändern.)

Ein Lambda-Ausdruck kann den Wert einer Variablen lesen, ohne ihn zu erfassen, wenn die Variable
* hat eine konstante non-volatile Ganzzahl oder einen Aufzählungstyp und wurde mit constexpr oder initialisiert
* ist constexpr und hat keine veränderlichen Mitglieder.

Seien Sie auf die Zukunft vorbereitet:

In C ++ 20 werden wir constexpr Standardalgorithmen und möglicherweise sogar einige Container haben, so dass constexpr Lambdas in diesem Zusammenhang sehr nützlich sein werden. Ihr Code sieht sowohl für die Laufzeitversion als auch für die constexpr Version (Version zur Kompilierungszeit) gleich aus!

Kurzgesagt:

constexpr lambda können Sie mit der Boilerplate-Programmierung konsistent sein und möglicherweise kürzeren Code haben.

Fahren wir nun mit der zweiten wichtigen Funktion fort, die in C ++ 17 verfügbar ist:

Erfassung von * diesem
Erfassen Sie * dies

Erinnerst du dich an unser Problem, als wir ein Mitglied der Klasse fangen wollten? Standardmäßig erfassen wir dies (als Zeiger!). Daher können Probleme auftreten, wenn temporäre Objekte den Gültigkeitsbereich verlassen ... Dies kann mithilfe der Erfassungsmethode mit einem Initialisierer behoben werden (siehe den ersten Teil der Serie). Aber jetzt, in C ++ 17, haben wir einen anderen Weg. Wir können eine Kopie davon einpacken:

 #include <iostream> struct Baz { auto foo() { return [*this] { 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

Das Erfassen der gewünschten Elementvariablen mithilfe der Erfassung mit dem Initialisierer schützt Sie vor möglichen Fehlern mit temporären Werten. Wir können jedoch nicht dasselbe tun, wenn wir eine Methode wie die folgende aufrufen möchten:

Zum Beispiel:

 struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; }; 

In C ++ 14 besteht die einzige Möglichkeit, Code sicherer zu machen, darin, this mit einem Initialisierer zu erfassen:

 auto foo() { return [self=*this] { self.print(); }; }   C ++ 17    : auto foo() { return [*this] { print(); }; } 

Noch etwas:

Beachten Sie, dass wenn Sie [=] in eine Member-Funktion schreiben, this implizit erfasst wird! Dies kann in Zukunft zu Fehlern führen ... und wird in C ++ 20 veraltet sein.

Also kommen wir zum nächsten Abschnitt: der Zukunft.

Die Zukunft mit C ++ 20

In C ++ 20 erhalten wir die folgenden Funktionen:

  • Erlaube [=, this] als Lambda-Erfassung - P0409R2 und brich die implizite Erfassung über [=] - P0806 ab
  • Paketerweiterung in lambda init-capture: ... args = std::move (args)] () {} - P0780
  • statische, thread_local und Lambda-Erfassung für strukturierte Bindungen - P1091
  • Lambda-Muster (auch mit Konzepten) - P0428R2
  • Vereinfachung der impliziten Lambda-Erfassung - P0588R1
  • Konstruktives und zuweisbares Lambda ohne Speichern des Standardzustands - P0624R2
  • Lambdas in einem nicht berechneten Kontext - P0315R4

In den meisten Fällen „löschen“ die neu eingeführten Funktionen die Lambda-Verwendung und ermöglichen einige erweiterte Anwendungsfälle.

Mit P1091 können Sie beispielsweise eine strukturierte Bindung erfassen.

Wir haben auch Erläuterungen dazu. In C ++ 20 erhalten Sie eine Warnung, wenn Sie [=] in einer Methode erfassen:

 struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20 

Wenn Sie dies wirklich erfassen müssen, sollten Sie [=, this] schreiben.

Es gibt auch Änderungen in Bezug auf erweiterte Anwendungsfälle, z. B. zustandslose Kontexte und zustandslose Lambdas, die standardmäßig erstellt werden können.

Mit beiden Änderungen können Sie schreiben:

 std::map<int, int, decltype([](int x, int y) { return x > y; })> map; 

Lesen Sie die Motive für diese Funktionen in der ersten Version der Sätze: P0315R0 und P0624R0 .

Aber schauen wir uns eine interessante Funktion an: Lambda-Vorlagen.

Lambd-Muster

In C ++ 14 haben wir verallgemeinerte Lambdas erhalten, was bedeutet, dass als auto deklarierte Parameter Vorlagenparameter sind.

Für Lambda:

 [](auto x) { x; } 

Der Compiler generiert eine Aufrufanweisung, die der folgenden Boilerplate-Methode entspricht:

 template<typename T> void operator(T x) { x; } 

Es gab jedoch keine Möglichkeit, diesen Vorlagenparameter zu ändern und die tatsächlichen Vorlagenargumente zu verwenden. In C ++ 20 ist dies möglich.

Wie können wir beispielsweise unser Lambda darauf beschränken, nur mit Vektoren irgendeiner Art zu arbeiten?

Wir können ein allgemeines Lambda schreiben:

 auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Wenn Sie es jedoch mit einem int-Parameter aufrufen (z. B. foo(10); ), wird möglicherweise ein schwer lesbarer Fehler angezeigt:

 prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n'; 

In C ++ 20 können wir schreiben:

 auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Das obige Lambda erlaubt die Template-Call-Anweisung:

 <typename T> void operator(std::vector<T> const& s) { ... } 

Der Template-Parameter folgt der Capture-Klausel [] .

Wenn Sie es mit int (foo(10);) aufrufen, erhalten Sie eine schönere Nachricht:

 note: mismatched types 'const std::vector<T>' and 'int' 


Sie können hier mit dem Code spielen: @Wandbox

Im obigen Beispiel kann der Compiler uns vor Inkonsistenzen in der Lambda-Schnittstelle als im Code im Body warnen.

Ein weiterer wichtiger Aspekt ist, dass Sie in einem universellen Lambda nur eine Variable haben, nicht deren Vorlagentyp. Wenn Sie darauf zugreifen möchten, müssen Sie daher decltype (x) verwenden (für einen Lambda-Ausdruck mit dem Argument (auto x)). Dies macht Code ausführlicher und komplizierter.

Zum Beispiel (unter Verwendung des Codes von P0428):

 auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; } 

Jetzt können Sie schreiben als:

 auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; } 

Im obigen Abschnitt hatten wir einen kurzen Überblick über C ++ 20, aber ich habe einen weiteren zusätzlichen Anwendungsfall für Sie. Diese Technik ist sogar in C ++ 14 möglich. Also lesen Sie weiter.

Bonus - Heben mit Lambdas

Wir haben derzeit ein Problem, wenn Sie Funktionsüberladungen haben und diese an Standardalgorithmen übergeben möchten (oder an alles, was ein genanntes Objekt erfordert):

 // two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); } 

Wir erhalten den folgenden Fehler von GCC 9 (Trunk):

 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^ 

Es gibt jedoch einen Trick, bei dem wir ein Lambda verwenden und dann die gewünschte Überlastfunktion aufrufen können.

In der Grundform können wir für einfache Werttypen für unsere beiden Funktionen den folgenden Code schreiben:

 std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 

Und in der allgemeinsten Form müssen wir etwas mehr eingeben:

 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \ -> decltype(foo(std::forward<decltype(x)>(x)...)) \ { return foo(std::forward<decltype(x)>(x)...); } 

Ziemlich komplizierter Code ... richtig? :) :)

Versuchen wir es zu entschlüsseln:

Wir erstellen ein generisches Lambda und übergeben dann alle Argumente, die wir erhalten. Um es richtig zu bestimmen, müssen wir noexcept und den Typ des Rückgabewerts angeben. Deshalb müssen wir den aufrufenden Code duplizieren, um die richtigen Typen zu erhalten.
Ein solches LIFT-Makro funktioniert in jedem Compiler, der C ++ 14 unterstützt.

Sie können hier mit dem Code spielen: @Wandbox

Fazit

In diesem Beitrag haben wir uns wichtige Änderungen in C ++ 17 angesehen und einen Überblick über die neuen Funktionen in C ++ 20 gegeben.

Möglicherweise stellen Sie fest, dass sich Lambda-Ausdrücke bei jeder Iteration der Sprache mit anderen C ++ - Elementen mischen. Zum Beispiel konnten wir sie vor C ++ 17 nicht im Kontext von constexpr verwenden, aber jetzt ist es möglich. Ähnlich verhält es sich mit generischen Lambdas, die mit C ++ 14 beginnen, und ihrer Entwicklung zu C ++ 20 in Form von Template-Lambdas. Vermisse ich etwas Vielleicht haben Sie ein aufregendes Beispiel? Bitte lassen Sie es mich in den Kommentaren wissen!

Referenzen

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
C ++ 17 - [expr.prim.lambda]
Lambda-Ausdrücke in C ++ | Microsoft-Dokumente
Simon Brand - Überlastsätze an Funktionen übergeben
Jason Turner - C ++ Weekly - Ep 128 - C ++ 20-Vorlagensyntax für Lambdas
Jason Turner - C ++ Weekly - Ep 41 - C ++ 17s Lambda-Support

Wir laden alle zu dem traditionellen kostenlosen Webinar des Kurses ein, das morgen, den 14. Juni, stattfinden wird.

Source: https://habr.com/ru/post/de455978/


All Articles