Lambdaausdrücke in C++

In C++11 und höher ist ein Lambda-Ausdruck , der häufig als Lambda bezeichnet wird, eine bequeme Möglichkeit, ein anonymes Funktionsobjekt (ein Schließen) direkt an der Stelle zu definieren, an der es aufgerufen oder als Argument an eine Funktion übergeben wird. In der Regel werden Lambdas verwendet, um einige Codezeilen zu kapseln, die an Algorithmen oder asynchrone Funktionen übergeben werden. In diesem Artikel wird definiert, was Lambdas sind, und sie mit anderen Programmiertechniken verglichen. Es beschreibt ihre Vorteile und enthält einige grundlegende Beispiele.

Teile eines Lambda-Ausdrucks

Hier ist eine einfache Lambda-Funktion, die als drittes Argument an die std::sort() Funktion übergeben wird:

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

Diese Abbildung zeigt die Teile der Lambda-Syntax:

Diagram that identifies the various parts of a lambda expression.

Das Lambda-Ausdrucksbeispiel ist [=]() mutable throw() -> int { return x+y; } [=] ist die Aufnahmeklausel; auch als Lambda-Einführung in der C++-Spezifikation bezeichnet. Die Klammer ist für die Parameterliste vorgesehen. Die änderbare Schlüsselwort (keyword) ist optional. throw() ist die optionale Ausnahmespezifikation. -> int ist der optionale nachfolgende Rückgabetyp. Der Lambda-Text besteht aus der Anweisung innerhalb der geschweiften geschweiften Klammern oder zurückgeben x+y; Diese werden im Anschluss an das Bild ausführlicher erläutert.

  1. Capture-Klausel (Auch bekannt als Lambda-Einführung in der C++-Spezifikation.)

  2. Parameterliste Optional. (Auch als Lambda-Deklarator bezeichnet)

  3. änderbare Spezifikation Optional.

  4. Ausnahmespezifikation Optional.

  5. Nachgestellter Rückgabetyp Optional.

  6. Lambda-Textkörper.

Capture-Klausel

Mit einer Lambda-Funktion können neue Variablen im Textkörper (in C++14) eingeführt werden, und sie kann auch auf Variablen aus dem umgebenden Bereich zugreifen oder diese erfassen. Eine Lambda-Funktion beginnt mit der Aufnahmeklausel. Es gibt an, welche Variablen erfasst werden, und ob die Erfassung nach Wert oder referenziert ist. Auf Variablen mit dem Präfix kaufmännisches Und-Zeichen (&) wird durch Verweis und Variablen zugegriffen, auf die sie nicht über einen Wert zugegriffen wird.

Eine leere Erfassungsklausel ([ ]) gibt an, dass der Lambda-Text auf keine Variablen im einschließenden Bereich zugreift.

Sie können einen Capture-Default-Modus verwenden, um anzugeben, wie externe Variablen erfasst werden, auf die im Lambda-Textkörper verwiesen wird: [&] bedeutet, dass alle Variablen, auf die Sie verweisen, durch Verweis erfasst werden, und [=] bedeutet, dass sie nach Wert erfasst werden. Sie können einen Standarderfassungsmodus verwenden, und dann den entgegengesetzten Modus explizit für bestimmte Variablen angeben. Wenn beispielsweise der Lambda-Text auf die externe Variable total nach Wert zugreift und auf die externe Variable factor nach Wert, dann sind die folgenden Erfassungsklauseln gleichwertig:

[&total, factor]
[factor, &total]
[&, factor]
[=, &total]

Nur Variablen, die im Lambda-Textkörper Erwähnung werden erfasst, wenn ein Aufnahmestandard verwendet wird.

Wenn eine Aufnahmeklausel einen Capture-Standard &enthält, kann kein Bezeichner in einer Erfassung dieser Aufnahmeklausel das Formular &identifieraufweisen. Ebenso kann, wenn die Aufnahmeklausel einen Capture-Standard =enthält, keine Erfassung dieser Aufnahmeklausel das Formular =identifieraufweisen. Ein Bezeichner oder this kann nicht mehr als einmal in einer Aufnahmeklausel angezeigt werden. Der folgende Codeausschnitt veranschaulicht einige Beispiele:

struct S { void f(int i); };

void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}

Eine Aufnahme, gefolgt von einer Auslassungspunkte, ist eine Packerweiterung, wie in diesem variadischen Vorlagenbeispiel gezeigt:

template<class... Args>
void f(Args... args) {
    auto x = [args...] { return g(args...); };
    x();
}

Um Lambda-Ausdrücke im Textkörper einer Klassenmemberfunktion zu verwenden, übergeben Sie den this Zeiger an die Aufnahmeklausel, um Zugriff auf die Memberfunktionen und Datenmember der eingeschlossenen Klasse zu ermöglichen.

Visual Studio 2017, Version 15.3 und höher (verfügbar im /std:c++17 Modus und höher): Der this Zeiger kann durch Angabe *this in der Aufnahmeklausel durch Wert erfasst werden. Die Erfassung nach Wert kopiert die gesamte Schließung an jede Anrufwebsite, auf der die Lambda-Funktion aufgerufen wird. (Ein Schließen ist das anonyme Funktionsobjekt, das den Lambda-Ausdruck kapselt.) Die Erfassung nach Wert ist nützlich, wenn die Lambda-Funktion in parallelen oder asynchronen Vorgängen ausgeführt wird. Es ist besonders nützlich für bestimmte Hardwarearchitekturen, z. B. NUMA.

Ein Beispiel, das zeigt, wie Lambda-Ausdrücke mit Klassenmememmfunktionen verwendet werden, finden Sie unter "Beispiel: Verwenden eines Lambda-Ausdrucks in einer Methode" in Beispielen für Lambda-Ausdrücke.

Wenn Sie die Aufnahmeklausel verwenden, sollten Sie diese Punkte berücksichtigen, insbesondere, wenn Sie Lambdas mit Multithreading verwenden:

  • Referenzerfassungen können verwendet werden, um Variablen außerhalb zu ändern, Aber Werteerfassungen können nicht. (mutable ermöglicht das Ändern von Kopien, aber nicht originalen.)

  • Referenzerfassungen spiegeln Aktualisierungen von Variablen außerhalb wider, werte erfassen jedoch nicht.

  • Verweiserfassungen führen eine Lebenszeitabhängigkeit ein, während Werterfassungen keine Lebenszeitabhängigkeiten haben. Es ist besonders wichtig, wenn die Lambda-Funktion asynchron ausgeführt wird. Wenn Sie einen lokalen Verweis in einer asynchronen Lambda-Funktion erfassen, kann diese lokal leicht nach der Ausführung der Lambda-Funktion verschwunden sein. Ihr Code könnte zur Laufzeit einen Zugriffsverstoß verursachen.

Generalisierte Erfassung (C++14)

In C++14 können Sie neue Variablen in der Aufnahmeklausel einführen und initialisieren, ohne dass diese Variablen im eingeschlossenen Bereich der Lambda-Funktion vorhanden sein müssen. Die Initialisierung kann mit einem beliebigen Ausdruck ausgedrückt werden; der Typ der neuen Variablen wird von dem durch den Ausdruck genierten Typ abgeleitet. Mit diesem Feature können Sie move-only-Variablen (z std::unique_ptr. B. ) aus dem umgebenden Bereich erfassen und in einer Lambda-Funktion verwenden.

pNums = make_unique<vector<int>>(nums);
//...
      auto a = [ptr = move(pNums)]()
        {
           // use ptr
        };

Parameterliste

Lambdas können Sowohl Variablen erfassen als auch Eingabeparameter akzeptieren. Eine Parameterliste (Lambda-Deklarator in der Standardsyntax) ist optional und in den meisten Aspekten ähnelt der Parameterliste für eine Funktion.

auto y = [] (int first, int second)
{
    return first + second;
};

Wenn der Parametertyp generisch ist, können Sie in C++14 den auto Schlüsselwort (keyword) als Typbezeichner verwenden. Dieser Schlüsselwort (keyword) weist den Compiler an, den Funktionsaufrufoperator als Vorlage zu erstellen. Jede Instanz auto in einer Parameterliste entspricht einem eindeutigen Typparameter.

auto y = [] (auto first, auto second)
{
    return first + second;
};

Ein Lambdaausdruck kann einen anderen Lambdaausdruck als Argument übernehmen. Weitere Informationen finden Sie unter "Lambda-Ausdrücke mit höherer Reihenfolge" im Artikel Beispiele für Lambda-Ausdrücke.

Da eine Parameterliste optional ist, können Sie die leeren Klammern weglassen, wenn Sie keine Argumente an den Lambda-Ausdruck übergeben und dessen Lambda-Deklarator keine Ausnahmespezifikation, nachgestellten Rückgabetyp oder mutable.

Veränderbare Spezifikation

In der Regel ist der Funktionsaufrufoperator einer Lambda-Funktion konstant, aber die Verwendung der mutable Schlüsselwort (keyword) bricht dies ab. Es werden keine veränderbaren Datenmember erzeugt. Die mutable Spezifikation ermöglicht es dem Textkörper eines Lambda-Ausdrucks, Variablen zu ändern, die nach Wert erfasst werden. Einige der Beispiele weiter unten in diesem Artikel zeigen, wie sie verwendet werden mutable.

Ausnahmespezifikation

Mit der noexcept Ausnahmespezifikation können Sie angeben, dass der Lambda-Ausdruck keine Ausnahmen auslöst. Wie bei normalen Funktionen generiert der Microsoft C++-Compiler die Warnung C4297 , wenn ein Lambda-Ausdruck die noexcept Ausnahmespezifikation deklariert und der Lambda-Textkörper eine Ausnahme auslöst, wie hier gezeigt:

// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
   []() noexcept { throw 5; }();
}

Weitere Informationen finden Sie unter Ausnahmespezifikationen (Throw).For more information, see Exception specifications (throw).

Rückgabetyp

Der Rückgabetyp von Lambda-Ausdrücken wird automatisch hergeleitet. Sie müssen die auto Schlüsselwort (keyword) nicht verwenden, es sei denn, Sie geben einen nachgestellten Rückgabetyp an. Der nachfolgende Rückgabetyp ähnelt dem Rückgabetypteil einer normalen Funktion oder Memberfunktion. Der Rückgabetyp folgt jedoch auf die Parameterliste, und das Schlüsselwort „trailing-return-type ->“ muss vor dem Rückgabetyp angegeben werden.

Sie können den Rückgabetypteil eines Lambda-Ausdrucks weglassen, wenn der Lambda-Textkörper nur eine Rückgabe-Anweisung enthält. Oder wenn der Ausdruck keinen Wert zurückgibt. Wenn der Lambda-Text aus einer einzelnen Rückgabeanweisung besteht, leitet der Compiler den Rückgabetyp vom Typ des Rückgabeausdrucks ab. Andernfalls leitet der Compiler den Rückgabetyp als void. Betrachten Sie die folgenden Beispielcodeausschnitte, die dieses Prinzip veranschaulichen:

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing
                                  // return type from braced-init-list isn't valid

Ein Lambdaausdruck kann einen anderen Lambdaausdruck als Rückgabewert erzeugen. Weitere Informationen finden Sie unter "Lambda-Ausdrücke mit höherer Reihenfolge" in Beispielen für Lambda-Ausdrücke.

Lambda-Textkörper

Der Lambda-Text eines Lambda-Ausdrucks ist eine zusammengesetzte Anweisung. Sie kann alles enthalten, was im Textkörper einer normalen Funktion oder Mitgliedsfunktion zulässig ist. Der Text einer gewöhnlichen Funktion und eines Lambdaausdrucks kann auf die folgenden Variablenarten zugreifen:

  • Aus dem einschließenden Bereich erfasste Variablen, wie zuvor beschrieben.

  • Parameter.

  • Lokal deklarierte Variablen.

  • Klassendatenmember, wenn sie innerhalb einer Klasse deklariert und this erfasst werden.

  • Jede Variable mit statischer Speicherdauer , z. B. globale Variablen.

Das folgende Beispiel enthält einen Lambdaausdruck, welcher explizit die Variable n nach ihrem Wert erfasst und implizit die Variable m als Verweis erfasst:

// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include <iostream>
using namespace std;

int main()
{
   int m = 0;
   int n = 0;
   [&, n] (int a) mutable { m = ++n + a; }(4);
   cout << m << endl << n << endl;
}
5
0

Da die Variable n als Wert erfasst wird, bleibt der Wert 0 nach dem Aufruf des Lambda-Ausdrucks erhalten. Die mutable Spezifikation ermöglicht n die Änderung innerhalb der Lambda-Funktion.

Ein Lambda-Ausdruck kann nur Variablen erfassen, die die automatische Speicherdauer aufweisen. Sie können jedoch Variablen verwenden, die statische Speicherdauer im Textkörper eines Lambda-Ausdrucks aufweisen. Im folgenden Beispiel wird die Funktion generate und ein Lambdaausdruck verwendet, um jedem Element in einem vector-Objekt einen Wert zuzuweisen. Der Lambda-Ausdruck ändert die statische Variable, um den Wert des nächsten Elements zu generieren.

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

Weitere Informationen finden Sie unter generieren.

Im folgenden Codebeispiel wird die Funktion aus dem vorherigen Beispiel verwendet, und es wird ein Beispiel für einen Lambda-Ausdruck hinzugefügt, der den C++-Standardbibliotheksalgorithmus verwendet generate_n. Dieser Lambdaausdruck weist ein Element eines vector-Objekts der Summe der vorangehenden zwei Elemente zu. Die mutable Schlüsselwort (keyword) wird verwendet, damit der Textkörper des Lambda-Ausdrucks seine Kopien der externen Variablen x ändern kann und ydie der Lambda-Ausdruck nach Wert erfasst. Da der Lambdaausdruck die originalen Variablen x und y als Wert erfasst, bleiben die Werte 1, nachdem das Lambda ausgeführt wird.

// compile with: /W4 /EHsc
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template <typename C> void print(const string& s, const C& c) {
    cout << s;

    for (const auto& e : c) {
        cout << e << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

int main()
{
    // The number of elements in the vector.
    const int elementCount = 9;

    // Create a vector object with each element set to 1.
    vector<int> v(elementCount, 1);

    // These variables hold the previous two elements of the vector.
    int x = 1;
    int y = 1;

    // Sets each element in the vector to the sum of the
    // previous two elements.
    generate_n(v.begin() + 2,
        elementCount - 2,
        [=]() mutable throw() -> int { // lambda is the 3rd parameter
        // Generate current value.
        int n = x + y;
        // Update previous two values.
        x = y;
        y = n;
        return n;
    });
    print("vector v after call to generate_n() with lambda: ", v);

    // Print the local variables x and y.
    // The values of x and y hold their initial values because
    // they are captured by value.
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(v);
    print("vector v after 1st call to fillVector(): ", v);
    // Fill the vector with the next sequence of numbers
    fillVector(v);
    print("vector v after 2nd call to fillVector(): ", v);
}
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18

Weitere Informationen finden Sie unter generate_n.

constexpr Lambda-Ausdrücke

Visual Studio 2017, Version 15.3 und höher (im /std:c++17 Modus und höher verfügbar): Sie können einen Lambda-Ausdruck als constexpr (oder in einem konstanten Ausdruck verwenden) deklarieren, wenn die Initialisierung der einzelnen erfassten oder eingeführten Datenmemm innerhalb eines konstanten Ausdrucks zulässig ist.

    int y = 32;
    auto answer = [y]() constexpr
    {
        int x = 10;
        return y + x;
    };

    constexpr int Increment(int n)
    {
        return [n] { return n + 1; }();
    }

Eine Lambda-Funktion ist implizit constexpr , wenn ihr Ergebnis den Anforderungen einer constexpr Funktion entspricht:

    auto answer = [](int n)
    {
        return 32 + n;
    };

    constexpr int response = answer(10);

Wenn eine Lambda-Funktion implizit oder explizit constexprerfolgt, erzeugt die Konvertierung in einen Funktionszeiger eine constexpr Funktion:

    auto Increment = [](int n)
    {
        return n + 1;
    };

    constexpr int(*inc)(int) = Increment;

Microsoft-spezifisch

Lambdas werden in den folgenden vom ClR verwalteten Entitäten (Common Language Runtime) nicht unterstützt: ref class, , , ref struct, value classoder value struct.

Wenn Sie einen Microsoft-spezifischen Modifizierer wie z __declspec. B. verwenden, können Sie ihn direkt hinter dem parameter-declaration-clause. Beispiel:

auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

Informationen dazu, ob ein bestimmter Modifizierer von Lambdas unterstützt wird, finden Sie im Artikel zum Modifizierer im Abschnitt "Microsoft-spezifische Modifizierer ".

Visual Studio unterstützt C++11 Standard Lambda-Funktionen und zustandslose Lambdas. Eine zustandslose Lambda-Funktion ist in einen Funktionszeiger konvertierbar, der eine beliebige Aufrufkonvention verwendet.

Siehe auch

C++-Programmiersprachenreferenz
Funktionsobjekte in der C++-Standardbibliothek
Funktionsaufruf
for_each